XNA для начинающих. Вершинный шейдер. Волны

XNA для начинающих. Вершинный шейдер. Волны

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

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

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


public
class
Game1 : Microsoft.Xna.Framework.Game

{


GraphicsDeviceManager graphics;


SpriteBatch spriteBatch;


VertexDeclaration vd;


VertexPositionColor[] vertices;


int[] indices;


int N = 120;


Effect effect;


public Game1()

{

graphics = new
GraphicsDeviceManager(this);

Content.RootDirectory = «Content»;

}


private
void CreateVertices()

{

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;

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

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>


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


CreateVertices();


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>(«waves»);

}


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


GraphicsDevice.RenderState.FillMode = FillMode.WireFrame;


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


Matrix View = Matrix.CreateLookAt(new
Vector3(0, 0, 2), Vector3.Zero, Vector3.Up);


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

GraphicsDevice.VertexDeclaration = vd;

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

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 создаются вершины, лежащие в узлах сетки. Вся поверхность будет располагаться в плоскости XZ в квадрате с координатами по диагонали от (0,0) до (1,-1). Сетка будет разбита вдоль каждой оси на N, то есть всего в сетке имеется N*N вершин.

В методе Draw, устанавливается режим рисования каркаса объектов, для того, чтобы были различимы вершины в сетке.

GraphicsDevice.RenderState.FillMode = FillMode.WireFrame;

Также нужно добавить новый файл с эффектом в папку Content (в моем случае он называется wave.fx) и модифицировать его следующим образом:

float4x4 World;

float4x4 View;

float4x4 Projection;

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


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


// TODO: add your vertex shader code here.


return output;

}

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0

{


// TODO: add your pixel shader code here.


return
float4(0, 0, 1, 1);

}

technique Waves

{


pass Pass1

{


// TODO: set renderstates here.


VertexShader = compile
vs_1_1 VertexShaderFunction();


PixelShader = compile
ps_1_1 PixelShaderFunction();

}

}

Здесь я переименовал технику с Technique1 в более осмысленное Waves и изменил цвет закраски пикселей на синий для того, чтобы имитировать водную поверхность.

Теперь посмотрим на то, что у нас получилось.

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

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

y(x,z,time) = sin(time + f(x,z))/k, где time – время, а k – нормирующих коэффициэнт.

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

y(x,z,time) = sin(time + f(x))/k1 + sin(time + g(z))/k2

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

Теперь наш вершинный шейдер будет выглядеть следующим образом:

float Time;

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)

{

VertexShaderOutput output;


float4 pos = input.Position;

float height = (sin(Time + input.Position.x * 50 + input.Position.z * 50))/25;

pos.y = height;


float4 worldPosition = mul(pos, World);


float4 viewPosition = mul(worldPosition, View);

output.Position = mul(viewPosition, Projection);


// TODO: add your vertex shader code here.


return output;

}

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

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


protected
override
void Draw(GameTime gameTime)

{

GraphicsDevice.Clear(Color.Black);


// TODO: Add your drawing code here


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


Matrix View = Matrix.CreateLookAt(new
Vector3(0, 0, 2), Vector3.Zero, Vector3.Up);


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

GraphicsDevice.VertexDeclaration = vd;

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

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

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

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


effect.Parameters[«Time»].SetValue((float)gameTime.TotalGameTime.TotalSeconds);

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

}

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

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

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

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

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 pos = input.Position;


float height = (sin(Time + input.Position.x * 50 + input.Position.z * 50))/25;

pos.y = height;


float4 worldPosition = mul(pos, World);


float4 viewPosition = mul(worldPosition, View);

output.Position = mul(viewPosition, Projection);


output.Height = height;


// TODO: add your vertex shader code here.


return output;

}

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0

{


// TODO: add your pixel shader code here.

// небольшое смещение для того, чтобы цвета были посветлее.


return
float4(0, 0, 1, 1) * (input.Height + 0.3f);

}

Обратите внимание на использование семантики TEXCOORD0 для Height, которая не является описанием текстурных координат, однако сейчас это не важно и на работу шейдера это не повлияет (Если Вы уже используете семантику TEXCOORD0 для этой структуры, то Вы можете воспользоваться TEXCOORD1, TEXCOORD2 и т.д. ).

При запуске приложения увидим следующую картину:

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

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

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

Логотип WordPress.com

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

Фотография Twitter

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

Фотография Facebook

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

Google+ photo

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

Connecting to %s