Cel Shading 4

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

Рассмотрим внимательно изображение из прошлого примера:

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

-4* [цвет текущего пикселя] + [цвет пикселя слева] + [цвет пикселя справа] + [цвет пикселя сверху] + [цвет пикселя снизу].

На самом деле можно легко упростить эту формулу вспомнив те причины, которые привели к ее созданию.


Итак, мы считаем, что точка лежит на границе разделяющей одноцветные области если цвета соседних с ней точек имеют разный цвет.

Ранее мы использовали формулу, в которой одновременно пытались учесть цвета всех пикселей в нужной области. На самом деле можно рассматривать различные случаи по очереди. Например, очевидно, что точка лежит на границы, если точки свева и справа от нее имеют разный цвет. Точно такое же заключение можно сделать и для точек снизу и сверху. Также, вместо точек слева, справа, снизу и сверху можно использовать точки, расположенные «по диагонали» относительно данной точки.

Такая операция называется «перекрестным оператором Робертса»


То есть, чтобы определить лежит ли С на границе нужно отдельно сравнить С1 с С4 и С2 с С3. Если разница цветов достаточно велика, то точка С лежит на границе.

Попробуем реализовать это в виде шейдера.

Сначала будем сравнивать С1 с С4 и рисовать границу

float4 PixelShaderFunction(float2 pos : TEXCOORD) : COLOR0

{

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


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

float2 deltaX = float2( 1.0 /400, 0);

float2 deltaY = float2( 0, 1.0 / 600);


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

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


float border = abs(color1 — color4);


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

drawColor.a = 1;

return drawColor;

}

Теперь тоже самое для C2 и С3.

float4 PixelShaderFunction(float2 pos : TEXCOORD) : COLOR0

{

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


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

float2 deltaX = float2( 1.0 /400, 0);

float2 deltaY = float2( 0, 1.0 / 600);


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

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


float border = abs(color2 — color3);


drawColor=float4(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( 1.0 /400, 0);

float2 deltaY = float2( 0, 1.0 / 600);


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

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

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

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


float border = abs(color1 — color4) + abs(color2 — color3);


drawColor=float4(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( 1.0 /400, 0);

float2 deltaY = float2( 0, 1.0 / 600);


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

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

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

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


float border = abs(color1 — color4) + abs(color2 — color3);


float result = 0;

if (border < 0.32)

{

result = 1;

}

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

drawColor.a = 1;

return drawColor;

}

Game1.cs (исходный код не должен был измениться по сравнению с прошлым примером)

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(1.0 / 400, 0);

float2 deltaY = float2( 0, 1.0 / 600);


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

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

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

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


float border = abs(color1 — color2) + abs(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