Cel Shading 7

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

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

Вначале изменим тип используемого нами примитива на куб:

teapot = new
CubePrimitive(GraphicsDevice);

Теперь мы видим, что некоторые ребра куба не выделены. Это происходит из-за того, что грани куба имеют одинаковый цвет, а наш алгоритм сейчас наход только границы одноцветных областей.

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

Соответственно, нам нужно создать новый шейдер, который будет «рисовать нормали». Сначала определимся с тем где и как мы будет их хранить.

Нормаль – это трехмерный вектор, а результатом работы пиксельного шейдера должен быть цвет, который, на самом деле, является четырехмерным вектором (Red, Green, Blue и альфа-канал). Таким образом мы можем хранить значения компонент нормали в разных канах выходного цвета, при этом у нас даже останется один свободный канал, который мы позже также используем.

float4x4 World;

float4x4 View;

float4x4 Projection;

// TODO: add effect parameters here.

struct VertexShaderInput

{

float4 Position : POSITION0;

float3 Normal : NORMAL;

// TODO: add input channels such as texture

// coordinates and vertex colors here.

};

struct VertexShaderOutput

{

float4 Position : POSITION0;

float4 Color : COLOR0;

// TODO: add vertex shader outputs such as colors and texture

// coordinates here. These values will automatically be interpolated

// over the triangle, and provided as input to your pixel shader.

};

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)

{

VertexShaderOutput output;

float4 worldPosition = mul(input.Position, World);

float4 viewPosition = mul(worldPosition, View);

output.Position = mul(viewPosition, Projection);


float3 worldNormal = normalize(mul(input.Normal, World));

output.Color.rgb = (worldNormal + 1) / 2.0;

output.Color.a = 1;

return output;

}

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0

{

return input.Color;

}

technique Technique1

{

pass Pass1

{

// TODO: set renderstates here.

VertexShader = compile vs_1_1 VertexShaderFunction();

PixelShader = compile ps_1_1 PixelShaderFunction();

}

}

Тут стоит обратить внимание на несколько моментов:

  1. float3 worldNormal = normalize(mul(input.Normal, World))

То есть нормаль пересчитывается в мировые координаты и нормализуется. В принципе, можно просто использовать input.Normal, то есть нормаль в локальных координатах.

  1. Я не передаю нормаль в пиксельный шейдер, а сразу в вершинном шейдере высчитываю цвет вершины. Это облегчит работу пиксельному шейдеру.
  2. output.Color.rgb = (worldNormal + 1) / 2.0

Тут я упаковываю значение нормали в диапазон от 0 до 1. Дело в том, что каждая составляющая нормали может принимать значение от -1 до 1, а отрицательные значения неприемнемы для цветов.

Применим наш шейдер. Будем рисовать нашел шейдером для рисования нормалей на новую поверхность.


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 / 4) * 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 / 4) * 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);


// DRAW NORMALS

GraphicsDevice.SetRenderTarget(0, normalTarget);

GraphicsDevice.Clear(Color.White);

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

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

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

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

teapot.Draw(normalEffect);

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

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

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

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

teapot.Draw(normalEffect);

GraphicsDevice.SetRenderTarget(0, null);

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

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();

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

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

spriteBatch.End();


base.Draw(gameTime);

}

В левой половине экрана изображение, полученное с использованим нашего Cel шейдера, в правой – с использованием нового шейдера рисования нормалей.

normalTarget.GetTexture() – текстура с нормалями.

Теперь нам нужен метод, который позволял бы найти границы областей с одинаковыми нормалями. Тут, как и в прошлый раз, мы можем воспользоваться оператором Робертса. Только нужно учитывать, что при работе с нормалями нас уже не интересует яркость пикселей (яркость нормали звучит забавно), теперь нам нужно именно значение цвета.

sampler Sampler : register(s0);

Texture normals;

sampler NormalSampler = sampler_state

{

Texture = <normals>;

};

float4 PixelShaderFunction(float2 pos : TEXCOORD) : COLOR0

{

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

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


float4 color1 = tex2D(NormalSampler, pos-deltaX-deltaY);

float4 color2 = tex2D(NormalSampler, pos-deltaX+deltaY);

float4 color3 = tex2D(NormalSampler, pos+deltaX-deltaY);

float4 color4 = tex2D(NormalSampler, pos+deltaX+deltaY);

float4 delta = abs(color1 — color4) + abs(color2 — color3);

float normalEdges = dot(delta.xyz, 1);

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

drawColor.a = 1;

return drawColor;

}

technique EdgeDetection

{

pass Pass1

{

// TODO: set renderstates here.

PixelShader = compile ps_2_0 PixelShaderFunction();

}

}

Единственная интересная строчка тут – это float normalEdges = dot(delta.xyz, 1);

В ней значение преобразуется из четырехмерного верктора в число типа float.

Воспользуемся новым шейдером, чтобы нарисовать края нашего куба, передав в него карту нормалей (текстуру с нормалями).

postEffect.Parameters[«normals»].SetValue(normalTarget.GetTexture());

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();

Практически все готово, осталось только объединить изображение с краями с нашим Cel шейдером.

float4 PixelShaderFunction(float2 pos : TEXCOORD) : COLOR0

{

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

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


float4 color1 = tex2D(NormalSampler, pos-deltaX-deltaY);

float4 color2 = tex2D(NormalSampler, pos-deltaX+deltaY);

float4 color3 = tex2D(NormalSampler, pos+deltaX-deltaY);

float4 color4 = tex2D(NormalSampler, pos+deltaX+deltaY);

float4 delta = abs(color1 — color4) + abs(color2 — color3);

float normalEdges = dot(delta.xyz, 1);

drawColor = tex2D(Sampler, pos) * (1-normalEdges);

drawColor.a = 1;

return drawColor;

}

Обратите внимание на то, что теперь все ребра куба выделены черный цветом, это именно то, что нам было нужно.

Пришло время вернуть на место чайник.

Исходный код:

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;


Effect normalEffect;


Texture2D lightMask;


RenderTarget2D target;


RenderTarget2D normalTarget;


Texture2D scene;


int height;


int width;


public Game1()

{

graphics = new
GraphicsDeviceManager(this);

Content.RootDirectory = «Content»;

width = graphics.PreferredBackBufferWidth = 800;

height = graphics.PreferredBackBufferHeight = 600;

}


///
<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);

normalTarget = new
RenderTarget2D(GraphicsDevice, graphics.PreferredBackBufferWidth,

graphics.PreferredBackBufferHeight, 1, SurfaceFormat.Color);


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»);

normalEffect = Content.Load<Effect>(«drawNormals»);

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 / 4) * 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 / 4) * 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);


// DRAW NORMALS

GraphicsDevice.SetRenderTarget(0, normalTarget);

GraphicsDevice.Clear(Color.White);

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

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

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

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

teapot.Draw(normalEffect);

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

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

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

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

teapot.Draw(normalEffect);

GraphicsDevice.SetRenderTarget(0, null);

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

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.Parameters[«normals»].SetValue(normalTarget.GetTexture());

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();


this.Window.Title = (1 / gameTime.ElapsedGameTime.TotalSeconds).ToString();


base.Draw(gameTime);

}

}

}

drawNormals.fx

float4x4 World;

float4x4 View;

float4x4 Projection;

// TODO: add effect parameters here.

struct VertexShaderInput

{

float4 Position : POSITION0;

float3 Normal : NORMAL;

// TODO: add input channels such as texture

// coordinates and vertex colors here.

};

struct VertexShaderOutput

{

float4 Position : POSITION0;

float4 Color : COLOR0;

// TODO: add vertex shader outputs such as colors and texture

// coordinates here. These values will automatically be interpolated

// over the triangle, and provided as input to your pixel shader.

};

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)

{

VertexShaderOutput output;

float4 worldPosition = mul(input.Position, World);

float4 viewPosition = mul(worldPosition, View);

output.Position = mul(viewPosition, Projection);


float3 worldNormal = normalize(mul(input.Normal, World));

output.Color.rgb = (worldNormal + 1) / 2.0;

output.Color.a = 1;

return output;

}

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0

{

return input.Color;

}

technique Technique1

{

pass Pass1

{

// TODO: set renderstates here.

VertexShader = compile vs_1_1 VertexShaderFunction();

PixelShader = compile ps_1_1 PixelShaderFunction();

}

}

PostEdge.fx

sampler Sampler : register(s0);

Texture normals;

sampler NormalSampler = sampler_state

{

Texture = <normals>;

};

float4 PixelShaderFunction(float2 pos : TEXCOORD) : COLOR0

{

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

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


float4 color1 = tex2D(NormalSampler, pos-deltaX-deltaY);

float4 color2 = tex2D(NormalSampler, pos-deltaX+deltaY);

float4 color3 = tex2D(NormalSampler, pos+deltaX-deltaY);

float4 color4 = tex2D(NormalSampler, pos+deltaX+deltaY);

float4 delta = abs(color1 — color4) + abs(color2 — color3);

float normalEdges = dot(delta.xyz, 1);

float4 drawColor = tex2D(Sampler, pos) * (1-normalEdges);

drawColor.a = 1;

return drawColor;

}

technique EdgeDetection

{

pass Pass1

{

// TODO: set renderstates here.

PixelShader = compile ps_2_0 PixelShaderFunction();

}

}

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

5 комментариев на «Cel Shading 7»

  1. Александр:

    Здравствуйте! А из какой библиотеки берется пространство имен Primitives3D ?

      • Александр:

        Спасибо за оперативный ответ! С этим разобрался, но возник еще ряд проблем при переносе кода в свой проект. Что-то я поправил, проект скомпилировался и запустился, но картинка вовсе не такая как у вас. Использую XNA 4.0
        1. У экземпляров Effect (например postEffect) не определены методы Begin(). Тоже самое с End().
        2. У объекта target нет метода GetTexture(). Можно ли обойтись приведением (Texture2D)target?

        И хотелось бы знать чем вызваны все эти проблемы? Вы писали для XNA 3.0?

  2. Статья 2010 года, так что, наверное да, на 3.0
    Вот полезная статья о переносе на 4 версию
    http://xnadev.ru/articles.php?article_id=103

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

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

Логотип WordPress.com

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

Фотография Twitter

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

Фотография Facebook

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

Google+ photo

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

Connecting to %s