Привет всем разработчикам игр и просто людям, которые интересуются геймдевом.
Пришло время рассказать вам о пиксельных шейдерах и о том, как сделать post-proccesing. Это вторая часть статьи о графических методах в XNA, в прошлой статье — мы рассматривали методы Draw и Begin у spriteBatch. Для примера: улучшим нашу систему частиц добавлением пиксельного шейдера, который будет искажать пространство.
В этой части:
Немного поговорим о шейдарах. Существуют два типа шейдера (в Shader Model 2.0, её то мы и используем): вертексный и пиксельный.
Вершинный шейдер оперирует данными, сопоставленными с вершинами многогранников. К таким данным, в частности, относятся координаты вершины в пространстве, текстурные координаты, тангенс-вектор, вектор бинормали, вектор нормали. Вершинный шейдер может быть использован для видового и перспективного преобразования вершин, генерации текстурных координат, расcчета освещения и т. д.
Пиксельные шейдеры выполняются для каждого фрагмента в фазе растеризации треугольников. Фрагмент (или пиксель) — точка, с оконными координатами, полученная растеризатором после выполнения над ней ряда операций. Про��е говоря, результирующая точка буфере кадра, совокупность этих точек потом формирует изображение. Пиксельные шейдеры оперируют над фрагментами до заключительных стадий, т.е. до тестов глубины, альфы и stencil. Пиксельный шейдер получает интерполированные данные (цвет, текстурные координаты) из вершинного шейдера.

Если сказать очень коротко про пиксельный шейдер, то это обработчик готового изображения.
В случае с Displacement-шейдером — вершинные шейдеры не нужны, рассмотрим пиксельные.
Если бытьленивым лаконичным человеком, то Post-processing шейдеры выполняются тогда, когда вся картинка игры уже отрисована: шейдер накладывается сразу на всю картинку, никак не на отдельные спрайты.
-Ведь у spriteBatch.Begin есть параметр, effect, не проще применять шейдер сразу, как мы его рисуем?
Отвечаю: вот именно, что такой шейдер применяется к единичным спрайтам, как итог, Displacement-шейдер будет функционировать криво.
Для создания Post-process обработки, нужно сначала рисовать то, что должно быть нарисовано на экране — на отдельную текстуру, а потом рисовать эту самую текстуру с использованием Post-process шейдера. Таким образом, шейдер воздействует не на единичные спрайты, а на картинку в целом.
-Стоп, а как рисовать на отдельную текстуру?
Отвечаю: знакомьтесь — RenderTarget2D
И опять, привет мой друг — лаконичность. RenderTarget2D — по сути является текстурой, на которую можно рисовать.
Идем туда, где обычно мы рисуем сцену, перед отчищением вставляем:
Теперь все будет рисоваться не на экран, а на RenderTarget2D.
Чтобы переключиться опять на экран, используем конструкцию:
Не забудьте очистить RenderTarget, перед прорисовкой.
Идея такого пиксельного шейдера очень проста: на вход поступает текстура, которую нужно «погнуть», на второй вход — карта, о том, как гнуть.
Карту мы будем генерировать, о том как — в практике.
Кстати, о карте. Карта представляет собой такое же по размерам изображение, как и текстура сцены, за исключением того, пожалуй, что нарисованного изображения — не увидим.
Более подобно о карте и о том, как действует шейдер:
В процессе обработки изображения — получаем текущую позицию пикселя, получаем цвет. Тоже самое делаем и для карты. Т.е. в конечном итоге, у нас будет доступно для модификации: цвет пикселя, позиция пикселя, цвет пикселя на карте соответствующего позиции пикселя на изображении.
Будем использовать цвета карты, чтобы передать информацию шейдеру, как погнуть пиксель.
К примеру, R-канал (красный) получает значения от 0f до 1f. Если мы видим на карте искажения R=0.5f, то просто сдвигаем позицию пикселя изображения на 10f * 0.5f пикселя. 10f — это сила, с которой мы сдвигаем.
Соответственно, R-канал будет соответствовать X координате, а G-канал — Y.
Если вам нужны картинки, получите их:
Исходная картинка:

Карта:

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

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

Копируем ParticleController и называем его ShaderController, в нем нам нужно изменить только сам процесс создания частицы, а конкретно:
Реализуем post-processing, создаем новые переменные:
Инициализируем их:
Идем к методу Draw главного класса и пишем:
Post-processing готов, теперь создадим шейдер.
Создаем новый Effect (fx) файл (это файл шейдера, написанного на HLSL), вписываем туда, что-то вроде:
Шейдер создан, загрузить его можно так же, как и обычную текстуру, за исключением того, что тип не Texture2D, а Effect.
Теперь обновим наш Draw:
Запускаем, любуемся красивыми, реалистичнымиживотными искажениями (лучше посмотреть демо):

На самом деле, эта реализация системы частиц (не шейдеры, а то что было в первом уроке) в целом — не совсем хороша для производительности. Сущесвтуют другие методы, более сложные в понимании, о них я расскажу как-нибудь потом.
Прикладываю исходники и демо (на этот раз, запустится на любом компьютере с XNA 4.0 и аппаратной подержкой DirectX9, inc sh 2.0)
Может быть на этой неделе, может быть неизвестно когда — расскажу о методе Update и как реализовать физику, используя Box2D.
Удачи вам и еще раз спраздником программиста 0xFF+1 днем! ;)
Пришло время рассказать вам о пиксельных шейдерах и о том, как сделать post-proccesing. Это вторая часть статьи о графических методах в XNA, в прошлой статье — мы рассматривали методы Draw и Begin у spriteBatch. Для примера: улучшим нашу систему частиц добавлением пиксельного шейдера, который будет искажать пространство.В этой части:
- Что такое пиксельный шейдер
- Что такое post-processing
- Кратко: Что такое RenderTarget2D и с чем его
едятзаправляют - Искажающий шейдер с Displacemenet-map
- Практика: дорабатываем систему частиц
Шейдер
Немного поговорим о шейдарах. Существуют два типа шейдера (в Shader Model 2.0, её то мы и используем): вертексный и пиксельный.
Вершинный шейдер оперирует данными, сопоставленными с вершинами многогранников. К таким данным, в частности, относятся координаты вершины в пространстве, текстурные координаты, тангенс-вектор, вектор бинормали, вектор нормали. Вершинный шейдер может быть использован для видового и перспективного преобразования вершин, генерации текстурных координат, расcчета освещения и т. д.
Пиксельные шейдеры выполняются для каждого фрагмента в фазе растеризации треугольников. Фрагмент (или пиксель) — точка, с оконными координатами, полученная растеризатором после выполнения над ней ряда операций. Про��е говоря, результирующая точка буфере кадра, совокупность этих точек потом формирует изображение. Пиксельные шейдеры оперируют над фрагментами до заключительных стадий, т.е. до тестов глубины, альфы и stencil. Пиксельный шейдер получает интерполированные данные (цвет, текстурные координаты) из вершинного шейдера.

Если сказать очень коротко про пиксельный шейдер, то это обработчик готового изображения.
В случае с Displacement-шейдером — вершинные шейдеры не нужны, рассмотрим пиксельные.
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.
Удачи вам и еще раз с
