XNA для начинающих. Пиксельный шейдер. Смешивание цветов.

XNA для начинающих. Пиксельный шейдер. Смешивание цветов.

До этого момента наши пиксельные шейдеры просто повторяли функциональность стандартного BasicEffect (а точнее, частично повторяли ее). В этой главе мы будем разрабатывать более сложный пиксельный шейдер для того, чтобы лучше разобраться с их работой. Ранее мы использовали только один цвет в пиксельном шейдере, мы получани его из текстуры либо из интереполированного значения цвета для вершин. Однако, это лишь наиболее простой способ получения цвета. Для получения более красивого изображения можно смешивать различные цвета в зависивости из каких-либо параметров пикселя.

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

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

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

Таким образом, нужно изменить способ отображения цветов на следующий:

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

Цвет = цвет_снега * вес_снега + цвет_горы * вес_горы + цвет_травы * все_травы + цвет_воды * вес_воды (Формула 1)

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

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

Weight(h) = 1-|(h – hmain)/hrange| (Формула 2) где h – относительная высота в точке h = hтекущая/hмаксимальная. hmain – это средняя высота из диапазона высот, составляющих определенный тип поверхности. hrange – это ширина диапазона.

Введем функции такого типа для каждого из видов поверхности и построим графики этих функций:

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

В этой карте высот более всетлым участкам соответствуют более высокие участки ландшафта.


public
class
Game1 : Microsoft.Xna.Framework.Game

{


GraphicsDeviceManager graphics;


SpriteBatch spriteBatch;


Effect effect;


VertexDeclaration vd;


VertexPositionColor[] vertices;


int[] indices;


int N = 120;


float maxHeight;


Texture2D heightMap;


public Game1()

{

graphics = new
GraphicsDeviceManager(this);

Content.RootDirectory = «Content»;

}


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

effect = Content.Load<Effect>(«landscape»);

heightMap = Content.Load<Texture2D>(«heightmap»);

CreateVertices();


// TODO: use this.Content to load your game content here

}


private
void CreateVertices()

{


Color[] heightMapColors = new
Color[heightMap.Width * heightMap.Width];

heightMap.GetData(heightMapColors);

N = heightMap.Width;

vd = new
VertexDeclaration(GraphicsDevice, VertexPositionColor.VertexElements);

vertices = new
VertexPositionColor[N * N];


float delta = 1f / (N — 1);


for (int i = 0; i < N; i++)


for (int j = 0; j < N; j++)

{


int index = i * N + j;


float height = heightMapColors[index].R / 255f;

vertices[index].Position = new
Vector3(delta * j, height, -delta * i);

maxHeight = Math.Max(maxHeight, height);

vertices[index].Color = Color.Blue;

}

indices = new
int[(N — 1) * (N — 1) * 6];


int counter = 0;


for (int z = 0; z < N — 1; z++)


for (int x = 0; x < N — 1; x++)

{


int lowerLeft = (z * N + x);


int lowerRight = lowerLeft + 1;


int upperLeft = lowerLeft + N;


int upperRight = upperLeft + 1;

indices[counter++] = lowerLeft;

indices[counter++] = upperLeft;

indices[counter++] = upperRight;

indices[counter++] = lowerLeft;

indices[counter++] = upperRight;

indices[counter++] = lowerRight;

}

}


///
<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.Clear(Color.Black);


// TODO: Add your drawing code here


Matrix World = Matrix.CreateTranslation(-0.5f,0,0.5f) * Matrix.CreateScale(3, 1, 3);


Matrix View = Matrix.CreateLookAt(new
Vector3(0, 1.5f, 3), Vector3.Zero, Vector3.Up);


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

GraphicsDevice.VertexDeclaration = vd;

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

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

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

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

effect.Begin();

effect.CurrentTechnique.Passes[0].Begin();

GraphicsDevice.DrawUserIndexedPrimitives<VertexPositionColor>(PrimitiveType.TriangleList, vertices, 0, N * N, indices, 0, indices.Length / 3);

effect.CurrentTechnique.Passes[0].End();

effect.End();


base.Draw(gameTime);

}

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

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

В пиксельном шейдере мы просто применим полученные ранее формулы:

float4x4 World;

float4x4 View;

float4x4 Projection;

float MaxHeight;

// TODO: add effect parameters here.

struct VertexShaderInput

{


float4 Position : POSITION0;


// TODO: add input channels such as texture


// coordinates and vertex colors here.

};

struct VertexShaderOutput

{


float4 Position : POSITION0;


float Height : TEXCOORD0;


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



// передаем высоту в локальных координатах

output.Height = input.Position.y;


// TODO: add your vertex shader code here.


return output;

}

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0

{


// TODO: add your pixel shader code here.


float snowW = clamp(1 — abs(input.Height/MaxHeight — 0.9)/0.2, 0, 1);

float rockW = clamp(1 — abs(input.Height/MaxHeight — 0.6)/0.3, 0, 1);

float grassW = clamp(1 — abs(input.Height/MaxHeight — 0.3)/0.3, 0, 1);

float waterW = clamp(1 — abs(input.Height/MaxHeight — 0.1)/0.2, 0, 1);


float4 snowColor = float4(1,1,1,1);

float4 rockColor = float4(0.64, 0.16, 0.16, 1);

float4 grassColor = float4(0, 0.5, 0, 1);

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


return snowW * snowColor +

rockW * rockColor +

grassW * grassColor +

waterW * waterColor;

}

technique Landscape

{


pass Pass1

{


// TODO: set renderstates here.


VertexShader = compile
vs_1_1 VertexShaderFunction();


PixelShader = compile
ps_1_1 PixelShaderFunction();

}

}

В пиксельном шейдере применяется несколько новых функций:

clamp(value, minValue, maxValue) – возвращает value, оно лежит в диалазоне от minValue до maxValue. В противном случае возвращает соответствующую границу диапазона (minValue или maxValue)

abs(value) – возвращает модуль числа value.

Итак, мы введи несколько переменных: snowW, rockW, grassW и waterW отвечают за веса снега, скалы, травы и воды соответственно, а snowColor, rockColor, grassColor и watercolor соответствуют цветам каждого типа поверхности. Итоговое значение цвета пикселя определяется по формуле 1. Значение весов были посчитаны по формуле 2.

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

error X4507: program too complex: exceeded available constant registers.

Дело в том, что теперь наш шейдер стал слишком сложным для шейдерного профиля 1.1 и я просто изменю это значение на 2.0

technique Landscape

{


pass Pass1

{


// TODO: set renderstates here.


VertexShader = compile
vs_1_1 VertexShaderFunction();


PixelShader = compile
ps_2_0 PixelShaderFunction();

}

}

Еще нужно изменить код метода Draw, задав значение MaxHeight:


protected
override
void Draw(GameTime gameTime)

{

GraphicsDevice.Clear(Color.Black);


// TODO: Add your drawing code here


Matrix World = Matrix.CreateTranslation(-0.5f,0,0.5f) * Matrix.CreateScale(3, 1, 3);


Matrix View = Matrix.CreateLookAt(new
Vector3(0, 1.5f, 3), Vector3.Zero, Vector3.Up);


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

GraphicsDevice.VertexDeclaration = vd;

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

effect.Parameters[«MaxHeight»].SetValue(maxHeight);

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

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

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

effect.Begin();

effect.CurrentTechnique.Passes[0].Begin();

GraphicsDevice.DrawUserIndexedPrimitives<VertexPositionColor>(PrimitiveType.TriangleList, vertices, 0, N * N, indices, 0, indices.Length / 3);

effect.CurrentTechnique.Passes[0].End();

effect.End();


base.Draw(gameTime);

}

При запуске получим ландшафт со смешиванием цветов.


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

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

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

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

Логотип WordPress.com

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

Фотография Twitter

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

Фотография Facebook

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

Google+ photo

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

Connecting to %s