Забавы с пиксельным шейдером. Toon, Toon с выделением границ.

Я уже писал массу статей о Toon шейдере, но все они работали с 3D моделями. В этой статье мы рассмотрим два шейдера, которые будут работать с 2D изображениями.

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

float4 PSToon( float2 Tex : TEXCOORD0 ) : COLOR0
{
float3 vecSkill1 = float3(3, 3, 3);
half4 Color = tex2D(ColorSampler,Tex.xy);
Color.r = round(Color.r*vecSkill1.x)/vecSkill1.x;
Color.g = round(Color.g*vecSkill1.y)/vecSkill1.y;
Color.b = round(Color.b*vecSkill1.z)/vecSkill1.z;
return Color;
}

technique Toon
{
pass Pass1
{
PixelShader = compile ps_2_0 PSToon();
}
}

Параметры эффекта задаются через вектор vecSkill1.

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

Используем двумерный фильтр, будем проходить по области 3х3 пикселя вокруг текущего и будем вычислять яркость. Дальше воспользуемся оператором Собеля, как мы делали это раньше.

Немного об операторе Собеля из википедии для того, чтобы освежить память:

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

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

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

Строго говоря, оператор использует ядра 3×3, с которыми свёртывают исходное изображение для вычисления приближенных значений производных по горизонтали и по вертикали. Пусть A исходное изображение, а Gx и Gy — два изображения, где каждая точка содержит приближенные производные по x и по y. Они вычисляются следующим образом:

где * обозначает двумерную операцию свертки.

Координата x здесь возрастает «направо», а y — «вниз». В каждой точке изображения приближенное значение величины градиента можно вычислить, используя полученные приближенные значения производных:

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


где, к примеру, угол Θ равен нулю для вертикальной границы, у которой тёмная сторона слева [Wiki].

Значение порога будет задаваться в последнем значении вектора vecSkill1.

 
float4 PSToon2( float2 Tex : TEXCOORD0 ) : COLOR0 
{                   
          float4 vecSkill1 = float4(3, 3, 3, 0.4);
 
          half4 color = tex2D(ColorSampler,Tex.xy);
          color.r = round(color.r*vecSkill1.r)/vecSkill1.r;
          color.g = round(color.g*vecSkill1.g)/vecSkill1.g;
          color.b = round(color.b*vecSkill1.b)/vecSkill1.b;
          
          const float threshold = vecSkill1.w;
 
          const int NUM = 9;
          const float2 c[NUM] =
          {
                    float2(-0.0078125, 0.0078125), 
                    float2( 0.00 ,     0.0078125),
                    float2( 0.0078125, 0.0078125),
                    float2(-0.0078125, 0.00 ),
                    float2( 0.0,       0.0),
                    float2( 0.0078125, 0.007 ),
                    float2(-0.0078125,-0.0078125),
                    float2( 0.00 ,    -0.0078125),
                    float2( 0.0078125,-0.0078125),
          };        
 
          int i;
          float3 col[NUM];
          for (i=0; i < NUM; i++)
          {
                    col[i] = tex2D(ColorSampler, Tex.xy + 0.2*c[i]);
          }
          
          float3 rgb2lum = float3(0.30, 0.59, 0.11);
          float lum[NUM];
          for (i = 0; i < NUM; i++)
          {
                    lum[i] = dot(col[i].xyz, rgb2lum);
          }
          float x = lum[2]+  lum[8]+2*lum[5]-lum[0]-2*lum[3]-lum[6];
          float y = lum[6]+2*lum[7]+  lum[8]-lum[0]-2*lum[1]-lum[2];
          float edge =(x*x + y*y < threshold)? 1.0:0.0;
          
          color.rgb *= edge;
          return color;
}
 
technique Toon2
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 PSToon2();
    }
}

Вообще 0.0078125 – это 1 / 128, более правильно было бы использовать 1 / 640 и 1 / 480, но в таком случае нужно будет устанавливать более низкий порог для определения границ.

Результат получится вот таким:

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

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;
 
namespace PixelShaderFun
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
 
        Texture2D background;
        Texture2D background2;
        Texture2D background3;
        Effect effect;
 
        int currentTehnique;
        KeyboardState oldKs;
 
        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
 
            graphics.PreferredBackBufferWidth = 640;
            graphics.PreferredBackBufferHeight = 480;
        }
 
        /// <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
 
            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
            background = Content.Load<Texture2D>("tulips");
            background2 = Content.Load<Texture2D>("koala");
            background3 = Content.Load<Texture2D>("flowers");
 
            effect = Content.Load<Effect>("pixelEffect");
        }
 
        /// <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
            KeyboardState ks = Keyboard.GetState();
            if (ks.IsKeyDown(Keys.Space) && oldKs.IsKeyUp(Keys.Space))
            {
                currentTehnique = (currentTehnique + 1) % effect.Techniques.Count;
                effect.CurrentTechnique = effect.Techniques[currentTehnique];
            }
 
            if (ks.IsKeyDown(Keys.Back) && oldKs.IsKeyUp(Keys.Back))
            {
                currentTehnique = (currentTehnique - 1);
                if (currentTehnique < 0) currentTehnique = effect.Techniques.Count - 1;
                effect.CurrentTechnique = effect.Techniques[currentTehnique];
            }
 
            oldKs = ks;
            Window.Title = effect.CurrentTechnique.Name;
            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.Clear(Color.Black);
 
            // TODO: Add your drawing code here
            effect.Parameters["time"].SetValue((float)gameTime.TotalGameTime.TotalSeconds);
            spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, null, null, null, effect);
            spriteBatch.Draw(background3, new Rectangle(0, 0, 640, 480), Color.White);
            spriteBatch.End();
 
            base.Draw(gameTime);
        }
    }
}

 

pixelEffect.fx (файл уже слишком
разросся, так что выкладываю только новые шейдеры)

sampler  ColorSampler  : register(s0);
float time;


float4 PSRed(float2 TexCoords : TEXCOORD0) : COLOR0
{
          float4 color1 = tex2D(ColorSampler, TexCoords);
 
          color1.g = 0;
          color1.b = 0;
 
          return color1;
}
 
float4 PSGreen(float2 TexCoords : TEXCOORD0) : COLOR0
{
          float4 color1 = tex2D(ColorSampler, TexCoords);
 
          color1.r = 0;
          color1.b = 0;
 
          return color1;
}
 
float4 PSBlue(float2 TexCoords : TEXCOORD0) : COLOR0
{
          float4 color1 = tex2D(ColorSampler, TexCoords);
 
          color1.g = 0;
          color1.r = 0;
 
          return color1;
}
 
 
technique DoNothing
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 donothing();
    }
}
 
technique Red
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 PSRed();
    }
}
technique Green
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 PSGreen();
    }
}
technique Blue
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 PSBlue();
    }
}

float4 PSToon( float2 Tex : TEXCOORD0 ) : COLOR0 
{                   
          float3 vecSkill1 = float3(3, 3, 3);
          half4 Color = tex2D(ColorSampler,Tex.xy);
          Color.r = round(Color.r*vecSkill1.x)/vecSkill1.x;
          Color.g = round(Color.g*vecSkill1.y)/vecSkill1.y;
          Color.b = round(Color.b*vecSkill1.z)/vecSkill1.z;
          return Color;
}
 
technique Toon
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 PSToon();
    }
}
 

float4 PSToon2( float2 Tex : TEXCOORD0 ) : COLOR0 
{                   
          float4 vecSkill1 = float4(3, 3, 3, 0.4);
 
          half4 color = tex2D(ColorSampler,Tex.xy);
          color.r = round(color.r*vecSkill1.r)/vecSkill1.r;
          color.g = round(color.g*vecSkill1.g)/vecSkill1.g;
          color.b = round(color.b*vecSkill1.b)/vecSkill1.b;
          
          const float threshold = vecSkill1.w;
 
          const int NUM = 9;
          const float2 c[NUM] =
          {
                    float2(-0.0078125, 0.0078125), 
                    float2( 0.00 ,     0.0078125),
                    float2( 0.0078125, 0.0078125),
                    float2(-0.0078125, 0.00 ),
                    float2( 0.0,       0.0),
                    float2( 0.0078125, 0.007 ),
                    float2(-0.0078125,-0.0078125),
                    float2( 0.00 ,    -0.0078125),
                    float2( 0.0078125,-0.0078125),
          };        
 
          int i;
          float3 col[NUM];
          for (i=0; i < NUM; i++)
          {
                    col[i] = tex2D(ColorSampler, Tex.xy + 0.2*c[i]);
          }
          
          float3 rgb2lum = float3(0.30, 0.59, 0.11);
          float lum[NUM];
          for (i = 0; i < NUM; i++)
          {
                    lum[i] = dot(col[i].xyz, rgb2lum);
          }
          float x = lum[2]+  lum[8]+2*lum[5]-lum[0]-2*lum[3]-lum[6];
          float y = lum[6]+2*lum[7]+  lum[8]-lum[0]-2*lum[1]-lum[2];
          float edge =(x*x + y*y < threshold)? 1.0:0.0;
          
          color.rgb *= edge;
          return color;
}
 
technique Toon2
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 PSToon2();
    }
}

 


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

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

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

Логотип WordPress.com

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

Фотография Twitter

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

Фотография Facebook

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

Google+ photo

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

Connecting to %s