Забавы с пиксельным шейдером. Цветовые плоскости RGB

В этот раз я хочу немного поработать с пиксельными шейдерами для 2D графики: создать несколько простых эффектов и, возможно, что-то полезное. Как это часто у меня бывает, это будет статья очень базового уровня.

Начнем мы как обычно с создания проекта и загрузки всех необходимых текстур.

        Texture2D background;
Texture2D background2;
Texture2D background3;

public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = «Content»;

graphics.PreferredBackBufferWidth = 640;
graphics.PreferredBackBufferHeight = 480;
}
/// <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>
/// 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);

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

Первый пиксельный шейдер будет просто рисовать оригинальное изображение

 
sampler  ColorSampler  : register(s0);

float4 donothing(float2 TexCoords : TEXCOORD0) : COLOR0
{
          return tex2D(ColorSampler, TexCoords);
}


technique PixelEffect
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 donothing();
    }
}

Я не случайно выбрал такое цветастое изображение. Дело в том, что первое, что я собираюсь сделать – это поработать с цветовыми плоскостями.

Итак, в XNA используется цветовая модель RGB. Вот что об этом говорит википедия:

RGB (аббревиатура английских слов Red, Green, Blue — красный, зелёный, синий) — аддитивная
цветовая модель, как правило, описывающая способ синтеза цвета для цветовоспроизведения. В российской традиции иногда обозначается как КЗС.

Выбор основных цветов обусловлен особенностями физиологии восприятия цвета сетчаткой человеческого глаза. Цветовая модель RGB нашла широкое применение в технике.

Аддитивной она называется потому, что цвета получаются путём добавления (англ.
addition) к черному. Иначе говоря, если цвет экрана, освещённого цветным прожектором, обозначается в RGB как (r1, g1, b1), а цвет того же экрана, освещенного другим прожектором, — (r2, g2, b2), то при освещении двумя прожекторами цвет экрана будет обозначаться как (r1+r2, g1+g2, b1+b2).

Изображение в данной цветовой модели состоит из трёх каналов. При смешении основных цветов (основными цветами считаются красный, зелёный и синий) — например, синего (B) и красного (R), мы получаем пурпурный (M magenta), при смешении зеленого (G) и красного (R) — жёлтый (Y yellow), при смешении зеленого (G) и синего (B) — циановый (С cyan). При смешении всех трёх цветовых компонентов мы получаем белый цвет (W).

В телевизорах и мониторах применяются три электронных пушки (светодиода, светофильтра) для красного, зелёного и синего каналов.

Цветовая модель RGB имеет по многим тонам цвета более широкий цветовой охват (может представить более насыщенные цвета), чем типичный охват цветов CMYK, поэтому иногда изображения, замечательно выглядящие в RGB, значительно тускнеют и гаснут в CMYK.

Джеймс Максвелл предложил аддитивный синтез цвета как способ получения цветных изображений в 1861 году.

Цветовая модель RGB была изначально разработана для описания цвета на цветном мониторе, но поскольку мониторы разных моделей и производителей различаются, были предложены несколько альтернативных цветовых моделей, соответствующих «усредненному» монитору. К таким относятся, например, sRGB и Adobe RGB.

Цветовая модель RGB может использовать разные оттенки основных цветов, разную цветовую температуру (задание «белой точки»), и разный показатель гамма-коррекции.

Представление базисных цветов RGB согласно рекомендациям ITU, в пространстве XYZ: Температура белого цвета: 6500 кельвинов (дневной свет)

Красный: x = 0,64 y = 0,33 
Зелёный: x = 0,29 y = 0,60 
Синий: x = 0,15 y = 0,06 

Матрицы для перевода цветов между системами RGB и XYZ (величину Y часто ставят в соответствие яркости при преобразовании изображения в чёрно-белое):

X = 0,431 * R + 0,342 * G + 0,178 * B 
Y = 0,222 * R + 0,707 * G + 0,071 * B 
Z = 0,020 * R + 0,130 * G + 0,939 * B 
R = 3,063 * X - 1,393 * Y - 0,476 * Z 
G = -0,969 * X + 1,876 * Y + 0,042 * Z 
B = 0,068 * X - 0,229 * Y + 1,069 * Z 

Теперь поговорим о числовом представлении модели RGB. Обычно в компьютерной графике границы допустимых значений каждого компонента цвета зависят от требуемого качества и доступного объема памяти. То есть под каждый пиксель изображения выделается некоторое количество бит, это количество делится (обычно равными долами) между всеми компонентами цвета. Например, если для представления пикселя тратится 3 байта (то есть 24 бита), то на R,G и B компоненты выделается по 1 байту. В таком случае можно считать, что R,G,B могут принимать целочисленные значения от 0 до 255, что соответствует насыщенности (больше означает насыщеннее) компонента. Таким образом можно закодировать более 16 миллионов цветов. Если мы захотим тратить меньше памяти, то можем использовать какое-либо другое представление схемы хранения данных о цвете пикселя, например, можно использовать 8 или 16 бит на пиксель. Тогда R,G,B будут иметь значения от 0 до 8 и от 0 до 16 соответственно (на самом деле, все не так просто, рекомендую ознакомиться с http://en.wikipedia.org/wiki/Color_depth). Когда мы работаем с шейдерами, для нас не должны быть важны параметры конкретной аппаратуры или операционной системы. Так что в пиксельном шейдере компоненты R,G и B могут принимать вещественные значения от 0 до 1 включительно.

Теперь пара слов о том, как получить доступ к компонентам цвета в пиксельном шейдере.

Цвета обычно имеют тип float4 (RGBA, то есть имеется альфа-канал, содержащей информацию о прозрачности пикселя). Обращаться к конкретным компонентам можно по первой букве имени канала.

Color.r- красный канал

Color.g – зеленый канал

Color.b – синий канал

Color.a – альфа-канал

Или же можно взять сразу несколько каналов:

Color.rgb, Color.bgr, Color.grb и т.д. возвращает float3 с со всеми компонентами. Обратите внимание на порядок следования букв, можно использовать произвольный поряд.

Color.rg, Color.rb, Color.bg, Color.br и т.д. вернет float2 с двумя заданными компонентами цвета.

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

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

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


int currentTehnique;
KeyboardState oldKs;

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

oldKs = ks;
Window.Title = effect.CurrentTechnique.Name;
base.Update(gameTime);
}

Посмотрим на результаты работы шейдеров:

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

Теперь посмотрим на картинки с цветами (цветы – это хороший для объяснения RGB схемы). Например, желтый цвет получается при смешивании красного и зеленого цветов, причем в чистом желтом цвете нет синего. На исходной картинке в нижней части есть несколько желтых цветов. Можно заметить, что в этом месте на красной и зеленой плоскостях есть белые очертания цветка, в то время как на синей плоскости желтые цветы полностью не видны. С другой стороны, фиолетовый цвет получает путем смешивания красного и синего. Фиолетовые цветы видны на красной и синей плоскостях, а не зеленой плоскости они значительно менее заметны.

Также можно отметить, например, то, что зеленые листья заметны только на зеленой плоскости.

Еще немного поработает с цветовыми плоскостями. В этот раз мы будем обнулять значение только в одном канале.

float4 PSNoRed(float2 TexCoords : TEXCOORD0) : COLOR0
{
float4 color1 = tex2D(ColorSampler, TexCoords);

color1.r = 0;

return color1;
}

float4 PSNoGreen(float2 TexCoords : TEXCOORD0) : COLOR0
{
float4 color1 = tex2D(ColorSampler, TexCoords);

color1.g = 0;

return color1;
}

float4 PSNoBlue(float2 TexCoords : TEXCOORD0) : COLOR0
{
float4 color1 = tex2D(ColorSampler, TexCoords);

color1.b = 0;

return color1;
}

technique NoRed
{
pass Pass1
{
PixelShader = compile ps_2_0 PSNoRed();
}
}
technique NoGreen
{
pass Pass1
{
PixelShader = compile ps_2_0 PSNoGreen();
}
}
technique NoBlue
{
pass Pass1
{
PixelShader = compile ps_2_0 PSNoBlue();
}
}

Вот результат:

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

Весь исходный код сейчас выглядит вот так:

pixelEffect.fx

sampler  ColorSampler  : register(s0);

float4 donothing(float2 TexCoords : TEXCOORD0) : COLOR0
{
return tex2D(ColorSampler, TexCoords);
}

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 PSNoRed(float2 TexCoords : TEXCOORD0) : COLOR0
{
float4 color1 = tex2D(ColorSampler, TexCoords);

color1.r = 0;

return color1;
}

float4 PSNoGreen(float2 TexCoords : TEXCOORD0) : COLOR0
{
float4 color1 = tex2D(ColorSampler, TexCoords);

color1.g = 0;

return color1;
}

float4 PSNoBlue(float2 TexCoords : TEXCOORD0) : COLOR0
{
float4 color1 = tex2D(ColorSampler, TexCoords);

color1.b = 0;

return color1;
}

technique NoRed
{
pass Pass1
{
PixelShader = compile ps_2_0 PSNoRed();
}
}
technique NoGreen
{
pass Pass1
{
PixelShader = compile ps_2_0 PSNoGreen();
}
}
technique NoBlue
{
pass Pass1
{
PixelShader = compile ps_2_0 PSNoBlue();
}
}

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];
}

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

            spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, nullnullnull, effect);
spriteBatch.Draw(background3, new Rectangle(0, 0, 640, 480), Color.White);
spriteBatch.End();

base.Draw(gameTime);
}
}
}

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

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

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

Логотип WordPress.com

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

Фотография Twitter

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

Фотография Facebook

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

Google+ photo

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

Connecting to %s