Cel Shading 5

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

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

Вспомним как работает модель освещения Фонга. По этой модели освещенность каждой точки объекта зависит от взаимной направленности нормали в точке и направления к источнику света. Если угол между нормалью и направлением к источнику света равен 90®, то такая точка находится в тени. Точнее диффузная составляющая освещенности будет равна 0. Если принять рассеянную и зеркальные составляющие за 0 для каждой точки, то точка полностью черной.

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

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

float4 PixelShaderFunctionPhongDiffuse(VertexShaderOutput input) : COLOR0

{

// TODO: add your pixel shader code here.

float3 worldPosition = input.WorldPosition;

float3 worldNormal = normalize(input.Normal);

float4 Ambient = ka * AmbientColor;

float3 lightDirection = normalize(LightPosition — worldPosition);

float4 Diffuse = kd * max(0, dot(worldNormal, lightDirection)) * DiffuseColor;

return Color + Ambient + Diffuse;

}

technique PhongDiffuse

{

pass Pass1

{

// TODO: set renderstates here.

VertexShader = compile vs_1_1 VertexShaderFunctionPhong();

PixelShader = compile ps_2_0 PixelShaderFunctionPhongDiffuse();

}

}

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

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

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

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

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

effect.Parameters[«LightPosition»].SetValue(cameraPosition);

effect.Parameters[«Color»].SetValue(new
Vector4(0, 0, 0, 1));

effect.Parameters[«AmbientColor»].SetValue(new
Vector4(0, 0, 0, 1));

teapot.Draw(effect);

Сейчас наш метод Draw может выглядеть вот так: (Я оставил рисование на промежуточную поверхность только потому, что оно пригодится нам позже, сейчас же оно совершенно не нужно)

protected
override
void Draw(GameTime gameTime)

{

GraphicsDevice.SetRenderTarget(0, target);

GraphicsDevice.Clear(Color.White);


// TODO: Add your drawing code here


Vector3 cameraPosition = new
Vector3(0, 0, 2);


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


Matrix view = Matrix.CreateLookAt(cameraPosition, Vector3.Zero, Vector3.Up);


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

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

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

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

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

effect.Parameters[«LightPosition»].SetValue(cameraPosition);

effect.Parameters[«Color»].SetValue(new
Vector4(0, 0, 0, 1));

effect.Parameters[«AmbientColor»].SetValue(new
Vector4(0, 0, 0, 1));

teapot.Draw(effect);

GraphicsDevice.SetRenderTarget(0, null);

GraphicsDevice.Clear(Color.White);


// edges

scene = target.GetTexture();

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

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

spriteBatch.End();


base.Draw(gameTime);

}


В рамках этого примера мы устанавливаем источник света в позицию камеры

effect.Parameters[«LightPosition»].SetValue(cameraPosition);

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

effect.Parameters[«DiffuseColor»].SetValue(new
Vector4(1, 1, 1, 1));

effect.Parameters[«kd»].SetValue(1f);

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

float4 PixelShaderFunctionPhongDiffuse(VertexShaderOutput input) : COLOR0

{

// TODO: add your pixel shader code here.

float3 worldPosition = input.WorldPosition;

float3 worldNormal = normalize(input.Normal);

float4 Ambient = ka * AmbientColor;

float3 lightDirection = normalize(LightPosition — worldPosition);

float4 Diffuse = kd * max(0, dot(worldNormal, lightDirection)) * DiffuseColor;

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

float4 result = float4(0,0,0,1);

if (dot(Color + Ambient + Diffuse, luminance) > 0.2)

{

result = float4(1,1,1,1);

}

return result;

}

Итак, мы получили границы для чайника. Но сейчас существует несколько проблем.

  1. Источник света находится в позиции камеры, хотя мы бы, конечно, хотели иметь возможность перемещать его в произвольную точку
  2. Сейчас используется модель освещения Фонга, а нам нужно использовать нашу Cel модкль освещения

Попробуем решить эти проблемы.

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

Сделаем это:

RenderTarget2D target1;

RenderTarget2D target2;

Texture2D scene1;

Texture2D scene2;

protected
override
void Initialize()

{

// TODO: Add your initialization logic here

teapot = new
TeapotPrimitive(GraphicsDevice);

target1 = new
RenderTarget2D(GraphicsDevice, graphics.PreferredBackBufferWidth,

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

target2 = new
RenderTarget2D(GraphicsDevice, graphics.PreferredBackBufferWidth,

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


base.Initialize();

}

protected
override
void Draw(GameTime gameTime)

{

GraphicsDevice.SetRenderTarget(0, target1);

GraphicsDevice.Clear(Color.White);


// TODO: Add your drawing code here


Vector3 cameraPosition = new
Vector3(0, 0, 2);


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


Matrix view = Matrix.CreateLookAt(cameraPosition, Vector3.Zero, Vector3.Up);


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

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

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

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

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

effect.Parameters[«LightPosition»].SetValue(cameraPosition);

effect.Parameters[«DiffuseColor»].SetValue(new
Vector4(1, 1, 1, 1));

effect.Parameters[«kd»].SetValue(1f); effect.Parameters[«Color»].SetValue(new
Vector4(0, 0, 0, 1));

effect.Parameters[«AmbientColor»].SetValue(new
Vector4(0, 0, 0, 1));

teapot.Draw(effect);

GraphicsDevice.SetRenderTarget(0, target2);

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

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

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

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

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

effect.Parameters[«Eye»].SetValue(cameraPosition);

effect.Parameters[«LightPosition»].SetValue(new
Vector3(0.5f, 0.5f, 1));

effect.Parameters[«Color»].SetValue(new
Vector4(0, 0, 0.3f, 1));

effect.Parameters[«DiffuseColor»].SetValue(new
Vector4(0, 0, 1, 1));

effect.Parameters[«AmbientColor»].SetValue(new
Vector4(0.1f, 0.1f, 0.1f, 1));

effect.Parameters[«kd»].SetValue(0.7f);

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

teapot.Draw(effect);

GraphicsDevice.SetRenderTarget(0, null);

GraphicsDevice.Clear(Color.White);


// edges

scene1 = target1.GetTexture();


// scene

scene2 = target2.GetTexture();

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

spriteBatch.Draw(scene1, new
Rectangle(0, 0, width , height), Color.White);

spriteBatch.End();


base.Draw(gameTime);

}

Теперь мы рисуем границы чайника на поверхность target1, а нашу сцену на поверхность target2.

Попробуем нарисовать сразу оба изображения (с обеих текстур):

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

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

spriteBatch.End();

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

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

spriteBatch.End();

К сожалению, изображение немного сжалось, но ничего страшного.

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

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

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 target1;


RenderTarget2D target2;


Texture2D scene1;


Texture2D scene2;


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

target1 = new
RenderTarget2D(GraphicsDevice, graphics.PreferredBackBufferWidth,

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

target2 = 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>(«postEdge2»);

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, target1);

GraphicsDevice.Clear(Color.White);


// TODO: Add your drawing code here


Vector3 cameraPosition = new
Vector3(0, 0, 2);


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


Matrix view = Matrix.CreateLookAt(cameraPosition, Vector3.Zero, Vector3.Up);


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

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

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

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

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

effect.Parameters[«LightPosition»].SetValue(cameraPosition);

effect.Parameters[«DiffuseColor»].SetValue(new
Vector4(1, 1, 1, 1));

effect.Parameters[«kd»].SetValue(1f);

effect.Parameters[«Color»].SetValue(new
Vector4(0, 0, 0, 1));

effect.Parameters[«AmbientColor»].SetValue(new
Vector4(0, 0, 0, 1));

teapot.Draw(effect);

GraphicsDevice.SetRenderTarget(0, target2);

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

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

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

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

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

effect.Parameters[«Eye»].SetValue(cameraPosition);

effect.Parameters[«LightPosition»].SetValue(new
Vector3(0.5f, 0.5f, 1));

effect.Parameters[«Color»].SetValue(new
Vector4(0, 0, 0.3f, 1));

effect.Parameters[«DiffuseColor»].SetValue(new
Vector4(0, 0, 1, 1));

effect.Parameters[«AmbientColor»].SetValue(new
Vector4(0.1f, 0.1f, 0.1f, 1));

effect.Parameters[«kd»].SetValue(0.7f);

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

teapot.Draw(effect);

GraphicsDevice.SetRenderTarget(0, null);

GraphicsDevice.Clear(Color.White);


// edges

scene1 = target1.GetTexture();


// scene

scene2 = target2.GetTexture();

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

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

spriteBatch.End();

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

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

spriteBatch.End();


base.Draw(gameTime);

}

}

}

Light.fx

float4x4 World;

float4x4 View;

float4x4 Projection;

float4 AmbientColor = float4(0, 0, 0, 1);

float ka = 0;

float4 DiffuseColor = float4(0,0,1,1);

float kd = 0.7;

float4 SpecularColor = float4(1,0,0,1);

float ks = 1;

float SpecularPower = 8;

float3 LightPosition = float3(0,0.5,1);

float3 Eye;

texture LightMask;

sampler LigthMaskSampler=sampler_state

{

texture = <LightMask>;

};

float4 Color = float4(0,0,0,1);

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

float3 WorldPosition : TEXCOORD0;

float3 Normal : TEXCOORD1;

// 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 VertexShaderFunctionPhong(VertexShaderInput input)

{

VertexShaderOutput output;

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

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

float4 viewPosition = mul(worldPosition, View);

output.Position = mul(viewPosition, Projection);

output.WorldPosition = worldPosition;

output.Normal = worldNormal;

// TODO: add your vertex shader code here.

return output;

}

float4 PixelShaderFunctionPhong(VertexShaderOutput input) : COLOR0

{

// TODO: add your pixel shader code here.

float3 worldPosition = input.WorldPosition;

float3 worldNormal = normalize(input.Normal);

float4 Ambient = ka * AmbientColor;

float3 lightDirection = normalize(LightPosition — worldPosition);

float4 Diffuse = kd * max(0, dot(worldNormal, lightDirection)) * DiffuseColor;

float3 eyeDirection = normalize(Eye — worldPosition);

float3 reflectedLight = normalize(reflect(-lightDirection, worldNormal));

float4 Specular = ks * pow(max(0, dot(eyeDirection, reflectedLight)), SpecularPower) * SpecularColor;

return Color + Ambient + Diffuse + Specular;

}

float4 PixelShaderFunctionPhongDiffuse(VertexShaderOutput input) : COLOR0

{

// TODO: add your pixel shader code here.

float3 worldPosition = input.WorldPosition;

float3 worldNormal = normalize(input.Normal);

float4 Ambient = ka * AmbientColor;

float3 lightDirection = normalize(LightPosition — worldPosition);

float4 Diffuse = kd * max(0, dot(worldNormal, lightDirection)) * DiffuseColor;

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

float4 result = float4(0,0,0,1);

if (dot(Color + Ambient + Diffuse, luminance) > 0.2)

{

result = float4(1,1,1,1);

}

return result;

}

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)

{

VertexShaderOutput output;

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

float4 viewPosition = mul(worldPosition, View);

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

output.Position = mul(viewPosition, Projection);

output.WorldPosition = worldPosition;

output.Normal = worldNormal;

// TODO: add your vertex shader code here.

return output;

}

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0

{

// TODO: add your pixel shader code here.

float3 worldPosition = input.WorldPosition;

float3 worldNormal = normalize(input.Normal);

float4 Ambient = ka * AmbientColor;

float3 lightDirection = normalize(LightPosition — worldPosition);

float Diffuse = kd * max(0, dot(worldNormal, lightDirection));

float3 eyeDirection = normalize(Eye — worldPosition);

float3 reflectedLight = normalize(reflect(-lightDirection, worldNormal));

float Specular = ks * pow(max(0, dot(eyeDirection, reflectedLight)), SpecularPower);

float2 pos= float2(0,0);

pos.x = Diffuse;

float4 dColor = tex2D(LigthMaskSampler, pos) * DiffuseColor;

pos.x = Specular;

float4 sColor = tex2D(LigthMaskSampler, pos) * SpecularColor;

// для экспериметов можно что-нибудь раскомментировать

//dColor = 0;

//Ambient = 0;

//sColor = 0;

return Color + Ambient + dColor + sColor ;

}

technique Phong

{

pass Pass1

{

// TODO: set renderstates here.

VertexShader = compile vs_1_1 VertexShaderFunctionPhong();

PixelShader = compile ps_2_0 PixelShaderFunctionPhong();

}

}

technique PhongDiffuse

{

pass Pass1

{

// TODO: set renderstates here.

VertexShader = compile vs_1_1 VertexShaderFunctionPhong();

PixelShader = compile ps_2_0 PixelShaderFunctionPhongDiffuse();

}

}

technique NPR

{

pass Pass1

{

// TODO: set renderstates here.

VertexShader = compile vs_1_1 VertexShaderFunction();

PixelShader = compile ps_2_0 PixelShaderFunction();

}

}

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

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

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

Логотип WordPress.com

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

Фотография Twitter

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

Фотография Facebook

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

Google+ photo

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

Connecting to %s