Cel Shader 3

О реализации некоторых нефотореалистичных эффектов. Часть 3

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

Результатом работы такого эффекта будет примерно следующее изображние (хотя обычно границы просто выделяются черным цветом, но я не смог найти подходящую картинку):

Перед тем как приступить к реализации эффекта выделения границ объекта я создам эффект выделения одноцветных областей объекта посколько такой эффект является более простым, но хорошо показывает принцип работы эффекта.

Еще до того как приступить к этому этапу я сделаю еще кое-что. Я заметил, что мои изображения недостаточно четкие, а границы объектов имеют слишком угловатую форму. Я применю возможности видео-карты чтобы максимально быстро и просто улучшить качество изображения. А для этого я включу multisampling.


public Game1()

{


// Наш старый код


graphics.PreferMultiSampling = true;

}

И обязательно нужно изменить параметры поверхности отображения RenderTarget так, чтобы они соответствовали параметрам экрана. В противном случае мы увидим исключение во время выполнения.


protected
override
void Initialize()

{


// Наш старый код

target = new
RenderTarget2D(GraphicsDevice, graphics.PreferredBackBufferWidth,

graphics.PreferredBackBufferHeight, 1, SurfaceFormat.Color, GraphicsDevice.PresentationParameters.MultiSampleType, GraphicsDevice.PresentationParameters.MultiSampleQuality);


base.Initialize();

}

Если ваша видео-карта поддерживает multisampling, то картинка должна улучшиться. Можно также изменять параметры multisampling через свойство graphics.GraphicsDevice. PresentationParameters.MultiSampleType, но в этом случае нужно убедиться в том, что целевая видео-карта поддерживает нужное качество, иначе вы получите ошибку. Например, это можно быть забавное сообщение «An unexpected error has occurred.»

Теперь собственно об эффекте, который мы будем делать. Итак, мы собирались создать эффект выделения границ одноцветных областей. Пожалуй, единственный вариант, который мы можем использовать в шейдере – это сравнение цвета текущего пикселя с цветами окружающих его пикселей. Рассмотрим простой пример

Будем пока рассматривать только цвета слева и справа от текущего пикселя. Получается, что для каждого пикселя из левой половины картинки левый и правый соседи имеют синий цвет, а для каждого пикселя из правой половины – красный.

А вот для каждого пикселя, находящегося на линии, разделяющей левую и правую половины, цвета соседей различаются. Таким образом можно, например, в результирующей картинке закрасить эту линию черным цветом.

В общем случае такое преобразование называется пространственной фильтрацией. При таком преобразовании итоговый цвет текущего пикселя зависит от цветов пикселей из некоторой окрестности.

Рассмотрим очень кратко пространственные фильтры. При пространственной фильтрации используется свертка при помощи «маски фильтра».

Допустим, мы хотим выполнить некоторое преобразование над цветом пикселя, выделенного кружком на рисунке. (Он имеет координаты (x,y)) Мы хотим, чтобы цвет пикселя зависел от цветом пикселей в прямоугольной окрестности с размерами i,j.

Для этого нужно взять свертку следующего вида:

Где:

F – новое значение цвета пикселя

P – цвет текущего пикселя

К – нормирующий коэффициент

М – маска фильтра

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


Попробуем понять, почему используется именно такая маска на небольших примерах. Итак, если цвета всех рассматриваемых пикселей одинаковы (пока будем считать, что цвет пикселя – это просто некоторое число), то значение F будет равно 0 (цвет пикселя в центре домножается на -4, цвета пикселей снизу, сверху, слева, справа домножаются на 1). Если же какой-либо пиксель из окрестности имеет цвет, отличающийся от других, то F уже не будет равно 0, из чего мы может заключить, что текущий пиксель находится на границе разделения одноцветных областей.

До этого мысчитали, что цвет некоторое число, на самом деле цвет обычно описывается в системе RGB, где каждому пикселю соответствую три числа, соответствующие красному (Red), зеленому (Green) и синему (Blue) компоненту цвета.

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

Получить яркость из цвета пикселя в формате RGB можно по следующей формуле:

Яркость = R*0.299 + G*0.587 + B*0.114 (да, зеленый цвет вносит наибольший вклад в яркость, а синий наименьший)

В шейдере такое преобразование будет иметь следующий вид:

float3 luminance = float3(0.299, 0.587, 0.114);

float Яркость = dot(Цвет, luminance);

Теперь начнем писать функцию пиксельного шейдера:

Нам нужно получить цвета соседних пикселей. Для этого добавим константы, соответствующие шагам по осям X и Y.

float2 deltaX = float2( float(1)/400, 0); // ширина экрана 800, но мы используем только половину, то есть 400

float2 deltaY = float2( 0, float(1) / 600); // высота экрана 600

Следующий код вычислит значения яркостей пикселей в окрестности текущего:

float color = dot(tex2D(Sampler, pos), luminance);

float color1 = dot(tex2D(Sampler, pos-deltaX), luminance);

float color2 = dot(tex2D(Sampler, pos+deltaX), luminance);

float color3 = dot(tex2D(Sampler, pos-deltaY), luminance);

float color4 = dot(tex2D(Sampler, pos+deltaY), luminance);

Теперь применим маску фильтра. Заодно сделаем операцию saturate, которая ограничивает аргумент 0 слева и 1 справа.

float border = saturate(-4*color + color1 + color2 + color3 + color4);

Теперь в border будет значение отличное от нуля для точек, лежащих на границах одноцветных областей. Чтобы рисовать границы церным цветом (на самом деле каким-то оттенком серого), нужно вычесть border из 1.

float4 PixelShaderFunction(float2 pos : TEXCOORD) : COLOR0

{

float3 luminance = float3(0.299, 0.587, 0.114);


// у нас половина изображения

float2 deltaX = float2( float(1)/400, 0);

float2 deltaY = float2( 0, float(1) / 600);


float color = dot(tex2D(Sampler, pos), luminance);

float color1 = dot(tex2D(Sampler, pos-deltaX), luminance);

float color2 = dot(tex2D(Sampler, pos+deltaX), luminance);

float color3 = dot(tex2D(Sampler, pos-deltaY), luminance);

float color4 = dot(tex2D(Sampler, pos+deltaY), luminance);


float border = saturate(-4*color + color1 + color2 + color3 + color4);

float4 drawColor=float4(1,1,1,1)* (1 – border);

drawColor.a = 1;

return drawColor;

}

Обратим внимание на то, что различные линии имеют разную яркость. Чем ярче линия, тем сильнее была разница в цветах в начальной картинке. Мы можем регулировать какие границы попадут в итоговое изображение введя дополнительный фильтр.

float4 PixelShaderFunction(float2 pos : TEXCOORD) : COLOR0

{

float3 luminance = float3(0.299, 0.587, 0.114);


// у нас половина изображения

float2 deltaX = float2( float(1)/400, 0);

float2 deltaY = float2( 0, float(1) / 600);


float color = dot(tex2D(Sampler, pos), luminance);

float color1 = dot(tex2D(Sampler, pos-deltaX), luminance);

float color2 = dot(tex2D(Sampler, pos+deltaX), luminance);

float color3 = dot(tex2D(Sampler, pos-deltaY), luminance);

float color4 = dot(tex2D(Sampler, pos+deltaY), luminance);


float border = saturate((-4*color + color1 + color2 + color3 + color4)*5);

float result = 0;

if (border < 0.01)

{

result = 1;

}

float4 drawColor = tex2D(Sampler, pos) * result;

drawColor.a = 1;

return drawColor;

}

Предел в данном случае выставлен равным 0.01, что означает то, что в итоговое изображение попадут даже очень слабые границы.


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

float result = 0;

if (border < 0.32)

{

result = 1;

}


Весь код выглядит следующим образом.

using System;

using System.Collections.Generic;

using System.Linq;

using Microsoft.Xna.Framework;

using Microsoft.Xna.Framework.Audio;

using Microsoft.Xna.Framework.Content;

using Microsoft.Xna.Framework.GamerServices;

using Microsoft.Xna.Framework.Graphics;

using Microsoft.Xna.Framework.Input;

using Microsoft.Xna.Framework.Media;

using Microsoft.Xna.Framework.Net;

using Microsoft.Xna.Framework.Storage;

using Primitives3D;

namespace NPR_1

{


///
<summary>


/// This is the main type for your game


///
</summary>


public
class
Game1 : Microsoft.Xna.Framework.Game

{


GraphicsDeviceManager graphics;


SpriteBatch spriteBatch;


GeometricPrimitive teapot;


Effect effect;


Effect postEffect;


Texture2D lightMask;


RenderTarget2D target;


Texture2D scene;


int height;


int width;


public Game1()

{

graphics = new
GraphicsDeviceManager(this);

Content.RootDirectory = «Content»;

width = graphics.PreferredBackBufferWidth = 800;

height = graphics.PreferredBackBufferHeight = 600;

graphics.PreferMultiSampling = true;

}


///
<summary>


/// Allows the game to perform any initialization it needs to before starting to run.


/// This is where it can query for any required services and load any non-graphic


/// related content. Calling base.Initialize will enumerate through any components


/// and initialize them as well.


///
</summary>


protected
override
void Initialize()

{


// TODO: Add your initialization logic here

teapot = new
TeapotPrimitive(GraphicsDevice);

target = new
RenderTarget2D(GraphicsDevice, graphics.PreferredBackBufferWidth,

graphics.PreferredBackBufferHeight, 1, SurfaceFormat.Color, GraphicsDevice.PresentationParameters.MultiSampleType, GraphicsDevice.PresentationParameters.MultiSampleQuality);


base.Initialize();

}


///
<summary>


/// LoadContent will be called once per game and is the place to load


/// all of your content.


///
</summary>


protected
override
void LoadContent()

{


// Create a new SpriteBatch, which can be used to draw textures.

spriteBatch = new
SpriteBatch(GraphicsDevice);


// TODO: use this.Content to load your game content here

effect = Content.Load<Effect>(«light»);

postEffect = Content.Load<Effect>(«postEdge»);

lightMask = Content.Load<Texture2D>(«lightMask»);

}


///
<summary>


/// UnloadContent will be called once per game and is the place to unload


/// all content.


///
</summary>


protected
override
void UnloadContent()

{


// TODO: Unload any non ContentManager content here

}


///
<summary>


/// Allows the game to run logic such as updating the world,


/// checking for collisions, gathering input, and playing audio.


///
</summary>


///
<param name=»gameTime»>Provides a snapshot of timing values.</param>


protected
override
void Update(GameTime gameTime)

{


// Allows the game to exit


if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)


this.Exit();


// TODO: Add your update logic here


base.Update(gameTime);

}


///
<summary>


/// This is called when the game should draw itself.


///
</summary>


///
<param name=»gameTime»>Provides a snapshot of timing values.</param>


protected
override
void Draw(GameTime gameTime)

{

GraphicsDevice.SetRenderTarget(0, target);

GraphicsDevice.Clear(new
Color(150, 150, 150));


// TODO: Add your drawing code here


Matrix world = Matrix.CreateRotationY((float)gameTime.TotalGameTime.TotalSeconds) * Matrix.CreateTranslation(-1.0f, 0, 0);


Matrix view = Matrix.CreateLookAt(new
Vector3(0, 1, 4), Vector3.Zero, Vector3.Up);


Matrix proj = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45), GraphicsDevice.Viewport.AspectRatio, 0.1f, 10);

effect.CurrentTechnique = effect.Techniques[«Phong»];

effect.Parameters[«World»].SetValue(world);

effect.Parameters[«View»].SetValue(view);

effect.Parameters[«Projection»].SetValue(proj);

effect.Parameters[«Eye»].SetValue(new
Vector3(0, 1, 4));

effect.Parameters[«LightMask»].SetValue(lightMask);

teapot.Draw(effect);

world = Matrix.CreateRotationY(-(float)gameTime.TotalGameTime.TotalSeconds) * Matrix.CreateTranslation(1.0f, 0, 0);

effect.CurrentTechnique = effect.Techniques[«NPR»];

effect.Parameters[«World»].SetValue(world);

effect.Parameters[«View»].SetValue(view);

effect.Parameters[«Projection»].SetValue(proj);

effect.Parameters[«Eye»].SetValue(new
Vector3(0, 1, 4));

effect.Parameters[«LightMask»].SetValue(lightMask);

teapot.Draw(effect);

GraphicsDevice.SetRenderTarget(0, null);

GraphicsDevice.Clear(Color.White);

scene = target.GetTexture();

spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate, SaveStateMode.SaveState);

spriteBatch.Draw(scene, new
Rectangle(0, 0, width / 2, height), new
Rectangle(0, 0, width / 2, height), Color.White);

spriteBatch.End();

postEffect.Begin();

spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate, SaveStateMode.SaveState);

postEffect.CurrentTechnique.Passes[0].Begin();

spriteBatch.Draw(scene, new
Rectangle(width / 2, 0, width / 2, height), new
Rectangle(width / 2, 0, width / 2, height), Color.White);

postEffect.CurrentTechnique.Passes[0].End();

postEffect.End();

spriteBatch.End();


base.Draw(gameTime);

}

}

}

Шейдер:

sampler Sampler;

float4 PixelShaderFunction(float2 pos : TEXCOORD) : COLOR0

{

float3 luminance = float3(0.299, 0.587, 0.114);


// у нас половина изображения

float2 deltaX = float2( float(1)/400, 0);

float2 deltaY = float2( 0, float(1) / 600);


float color = dot(tex2D(Sampler, pos), luminance);

float color1 = dot(tex2D(Sampler, pos-deltaX), luminance);

float color2 = dot(tex2D(Sampler, pos+deltaX), luminance);

float color3 = dot(tex2D(Sampler, pos-deltaY), luminance);

float color4 = dot(tex2D(Sampler, pos+deltaY), luminance);


float border = saturate(-4*color + color1 + color2 + color3 + color4);

float result = 0;

if (border < 0.32)

{

result = 1;

}

float4 drawColor = tex2D(Sampler, pos) * result;

drawColor.a = 1;

return drawColor;

}

technique EdgeDetection

{

pass Pass1

{

// TODO: set renderstates here.

PixelShader = compile ps_2_0 PixelShaderFunction();

}

}

Реклама
Запись опубликована в рубрике Uncategorized с метками , , , , . Добавьте в закладки постоянную ссылку.

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s