NPR_cel3

advertisement
Иван Андреев
andreev.ia@gmail.com
О реализации некоторых нефотореалистичных эффектов. Часть 3
Следующий эффект, который мы будем создавать – это выделение границ объекта, такая техника очень
часто (практически всегда) применяется в рисованой графике. Обычно рисование начинается с определения
контуров объекта и, возможно, границ одноцветных областей, затем уже выполняется заполнение цветами,
а граница остается на результирующем изображении.
Результатом работы такого эффекта будет примерно следующее изображние (хотя обычно границы просто
выделяются черным цветом, но я не смог найти подходящую картинку):
Перед тем как приступить к реализации эффекта выделения границ объекта я создам эффект выделения
одноцветных областей объекта посколько такой эффект является более простым, но хорошо показывает
принцип работы эффекта.
Еще до того как приступить к этому этапу я сделаю еще кое-что. Я заметил, что мои изображения
недостаточно четкие, а границы объектов имеют слишком угловатую форму. Я применю возможности
видео-карты чтобы максимально быстро и просто улучшить качество изображения. А для этого я включу
multisampling.
1
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.»
Теперь собственно об эффекте, который мы будем делать. Итак, мы собирались создать эффект выделения
границ одноцветных областей. Пожалуй, единственный вариант, который мы можем использовать в
шейдере – это сравнение цвета текущего пикселя с цветами окружающих его пикселей. Рассмотрим простой
пример
Будем пока рассматривать только цвета слева и справа от текущего пикселя. Получается, что для каждого
пикселя из левой половины картинки левый и правый соседи имеют синий цвет, а для каждого пикселя из
правой половины – красный.
А вот для каждого пикселя, находящегося на линии, разделяющей левую и правую половины, цвета соседей
различаются. Таким образом можно, например, в результирующей картинке закрасить эту линию черным
цветом.
В общем случае такое преобразование называется пространственной фильтрацией. При таком
преобразовании итоговый цвет текущего пикселя зависит от цветов пикселей из некоторой окрестности.
Рассмотрим очень кратко пространственные фильтры. При пространственной фильтрации используется
свертка при помощи «маски фильтра».
2
Допустим, мы хотим выполнить некоторое преобразование над цветом пикселя, выделенного кружком на
рисунке. (Он имеет координаты (x,y)) Мы хотим, чтобы цвет пикселя зависел от цветом пикселей в
прямоугольной окрестности с размерами i,j.
Для этого нужно взять свертку следующего вида:
Где:
F – новое значение цвета пикселя
P – цвет текущего пикселя
К – нормирующий коэффициент
М – маска фильтра
В зависимости от маски фильтра мы будем получать различные эффетры. Например, для выделения границ
можно использовать следующую маску (на самом деле существует большое количество различных методов
выделения границ, основанных на фильтрах, однако тут мы рассмотрим только этот способ):
Попробуем понять, почему используется именно такая маска на небольших примерах. Итак, если цвета всех
рассматриваемых пикселей одинаковы (пока будем считать, что цвет пикселя – это просто некоторое
число), то значение F будет равно 0 (цвет пикселя в центре домножается на 4, цвета пикселей снизу, сверху,
слева, справа домножаются на -1). Если же какой-либо пиксель из окрестности имеет цвет, отличающийся
от других, то F уже не будет равно 0, из чего мы может заключить, что текущий пиксель находится на
границе разделения одноцветных областей.
3
До этого мысчитали, что цвет некоторое число, на самом деле цвет обычно описывается в системе 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
float
float
float
float
color = dot(tex2D(Sampler, pos), luminance);
color1 = dot(tex2D(Sampler, pos-deltaX), luminance);
color2 = dot(tex2D(Sampler, pos+deltaX), luminance);
color3 = dot(tex2D(Sampler, pos-deltaY), luminance);
color4 = dot(tex2D(Sampler, pos+deltaY), luminance);
Теперь применим маску фильтра
float border = (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
float
float
float
float
color = dot(tex2D(Sampler, pos), luminance);
color1 = dot(tex2D(Sampler, pos-deltaX), luminance);
color2 = dot(tex2D(Sampler, pos+deltaX), luminance);
color3 = dot(tex2D(Sampler, pos-deltaY), luminance);
color4 = dot(tex2D(Sampler, pos+deltaY), luminance);
float border = (4*color - color1 - color2 - color3 - color4);
float4 drawColor=float4(1,1,1,1)* (1 – border);
drawColor.a = 1;
return drawColor;
}
4
Обратим внимание на то, что различные линии имеют разную яркость. Чем ярче линия, тем сильнее была
разница в цветах в начальной картинке. Мы можем регулировать какие границы попадут в итоговое
изображение введя доплнительный фильтр.
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
float
float
float
float
color = dot(tex2D(Sampler, pos), luminance);
color1 = dot(tex2D(Sampler, pos-deltaX), luminance);
color2 = dot(tex2D(Sampler, pos+deltaX), luminance);
color3 = dot(tex2D(Sampler, pos-deltaY), luminance);
color4 = dot(tex2D(Sampler, pos+deltaY), luminance);
float border = (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, что означает то, что в итоговое изображение попадут даже
очень слабые границы.
5
Если установить большее значение, то в итоговое изображение будут попадать только более сильные
границы.
float result = 0;
if (border < 0.32)
{
result = 1;
}
Весь код выглядит следующим образом.
using
using
using
using
using
using
using
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
Microsoft.Xna.Framework;
Microsoft.Xna.Framework.Audio;
Microsoft.Xna.Framework.Content;
Microsoft.Xna.Framework.GamerServices;
Microsoft.Xna.Framework.Graphics;
Microsoft.Xna.Framework.Input;
Microsoft.Xna.Framework.Media;
Microsoft.Xna.Framework.Net;
Microsoft.Xna.Framework.Storage;
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
6
/// 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);
7
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
float
float
float
float
color = dot(tex2D(Sampler, pos), luminance);
color1 = dot(tex2D(Sampler, pos-deltaX), luminance);
color2 = dot(tex2D(Sampler, pos+deltaX), luminance);
color3 = dot(tex2D(Sampler, pos-deltaY), luminance);
color4 = dot(tex2D(Sampler, pos+deltaY), luminance);
float border = (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();
}
}
8
Download