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

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

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

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

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

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

Возьмем за основу код из прошлой главы и модифицируем его следующим образом:


public
class
Game1 : Microsoft.Xna.Framework.Game

{


GraphicsDeviceManager graphics;


SpriteBatch spriteBatch;


Effect effect;


VertexDeclaration vd;


VertexPositionTexture[] vertices;


int[] indices;


int N = 120;


float maxHeight;


Texture2D heightMap;


Texture2D sandTexture;


Texture2D snowTexture;


Texture2D rockTexture;


Texture2D grassTexture;


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

grassTexture = Content.Load<Texture2D>(«grass»);

snowTexture = Content.Load<Texture2D>(«snow»);

sandTexture = Content.Load<Texture2D>(«sand»);

rockTexture = Content.Load<Texture2D>(«rock»);

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, VertexPositionTexture.VertexElements);

vertices = new
VertexPositionTexture[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].TextureCoordinate = new
Vector2(delta * j, delta * i);

}

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[«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<VertexPositionTexture>(PrimitiveType.TriangleList, vertices, 0, N * N, indices, 0, indices.Length / 3);

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

effect.End();


base.Draw(gameTime);

}

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

float4x4 World;

float4x4 View;

float4x4 Projection;

float MaxHeight;

Texture snow;

Texture rock;

Texture grass;

Texture sand;

sampler snowSampler = sampler_state

{

Texture = <snow>;

};

sampler rockSampler = sampler_state

{

Texture = <rock>;

};

sampler grassSampler = sampler_state

{

Texture = <grass>;

};

sampler sandSampler = sampler_state

{

Texture = <sand>;

};

// TODO: add effect parameters here.

struct VertexShaderInput

{


float4 Position : POSITION0;


float2 TexCoords : TEXCOORD0;


// TODO: add input channels such as texture


// coordinates and vertex colors here.

};

struct VertexShaderOutput

{


float4 Position : POSITION0;


float Height : TEXCOORD0;


float2 TexCoords : 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 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;

output.TexCoords = input.TexCoords;


// 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 sandW = clamp(1 — abs(input.Height/MaxHeight — 0.1)/0.2, 0, 1);


float4 rockColor = tex2D(rockSampler, input.TexCoords);

float4 snowColor = tex2D(snowSampler, input.TexCoords);

float4 grassColor = tex2D(grassSampler, input.TexCoords);

float4 sandColor = tex2D(sandSampler, input.TexCoords);


return snowW * snowColor + rockW * rockColor + grassW * grassColor + sandW * sandColor;

}

technique Landscape

{


pass Pass1

{


// TODO: set renderstates here.


VertexShader = compile
vs_1_1 VertexShaderFunction();


PixelShader = compile
ps_2_0 PixelShaderFunction();

}

}

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

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 sandW = clamp(1 — abs(input.Height/MaxHeight — 0.1)/0.2, 0, 1);


float4 rockColor = tex2D(rockSampler, input.TexCoords);

float4 snowColor = tex2D(snowSampler, input.TexCoords*2);

float4 grassColor = tex2D(grassSampler, input.TexCoords*3);

float4 sandColor = tex2D(sandSampler, input.TexCoords*0.5);


return snowW * snowColor + rockW * rockColor + grassW * grassColor + sandW * sandColor;

}


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


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.Parameters[«snow»].SetValue(snowTexture);

effect.Parameters[«rock»].SetValue(rockTexture);

effect.Parameters[«grass»].SetValue(grassTexture);

effect.Parameters[«sand»].SetValue(sandTexture);

effect.Begin();

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

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

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

effect.End();


base.Draw(gameTime);

}

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

Тогда код CreateVertices будет выглядеть следующим образом:


VertexPositionColorTexture[] vertices;


private
void CreateVertices()

{


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

heightMap.GetData(heightMapColors);

N = heightMap.Width;


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

vertices = new
VertexPositionColorTexture[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].TextureCoordinate = new
Vector2(delta * j, delta * i);

}


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


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

{


int index = i * N + j;


float snowW = (float)MathHelper.Clamp(1f — Math.Abs(vertices[index].Position.Y / maxHeight — 0.9f) / 0.2f, 0f, 1f);


float rockW = (float)MathHelper.Clamp(1f — Math.Abs(vertices[index].Position.Y / maxHeight — 0.6f) / 0.3f, 0f, 1f);


float grassW = (float)MathHelper.Clamp(1f — Math.Abs(vertices[index].Position.Y / maxHeight — 0.3f) / 0.3f, 0f, 1f);


float waterW = (float)MathHelper.Clamp(1f — Math.Abs(vertices[index].Position.Y / maxHeight — 0.1f) / 0.2f, 0f, 1f);

vertices[index].Color = new
Color(snowW, rockW, grassW, waterW);

}

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;

}

}

Шейдер будет иметь следующий вид:

float4x4 World;

float4x4 View;

float4x4 Projection;

float MaxHeight;

Texture snow;

Texture rock;

Texture grass;

Texture sand;

sampler snowSampler = sampler_state

{

Texture = <snow>;

};

sampler rockSampler = sampler_state

{

Texture = <rock>;

};

sampler grassSampler = sampler_state

{

Texture = <grass>;

};

sampler sandSampler = sampler_state

{

Texture = <sand>;

};

// TODO: add effect parameters here.

struct VertexShaderInput

{


float4 Position : POSITION0;


float2 TexCoords : TEXCOORD0;


float4 Color : COLOR0;


// TODO: add input channels such as texture


// coordinates and vertex colors here.

};

struct VertexShaderOutput

{


float4 Position : POSITION0;


float Height : TEXCOORD0;


float2 TexCoords : TEXCOORD1;


float4 Color : COLOR0;


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

output.TexCoords = input.TexCoords;

output.Color = input.Color;


// TODO: add your vertex shader code here.


return output;

}

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0

{


// TODO: add your pixel shader code here.

float snowW = input.Color.r;

float rockW = input.Color.g;

float grassW = input.Color.b;

float sandW = input.Color.a;


float4 rockColor = tex2D(rockSampler, input.TexCoords);

float4 snowColor = tex2D(snowSampler, input.TexCoords);

float4 grassColor = tex2D(grassSampler, input.TexCoords);

float4 sandColor = tex2D(sandSampler, input.TexCoords);


return snowW * snowColor + rockW * rockColor + grassW * grassColor + sandW * sandColor;

}

technique Landscape

{


pass Pass1

{


// TODO: set renderstates here.


VertexShader = compile
vs_1_1 VertexShaderFunction();


PixelShader = compile
ps_2_0 PixelShaderFunction();

}

}

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

4 комментария на «XNA для начинающих. Пиксельный шейдер. Мультитекстурирование.»

  1. dixus:

    Приветствую!
    В своей статье вы пишите:
    «Возьмем за основу код из прошлой главы и модифицируем…»
    Не могли бы вы уточнить о какой именно «прошлой главе» идет речь или дать на нее ссылку.
    Спасибо.

  2. Владимир:

    Спасибо, интересная статья. Вот бы ещё узнать каким образом можно было бы создать редактор в котором можно рисовать кистью текстуры прям по ландшафту.

    • Вот так в двух словах этого не объяснить. Туториалов я сходу найти тоже не смог, но, думаю, нагуглить что-нибудь можно.

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

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

Логотип WordPress.com

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

Фотография Twitter

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

Фотография Facebook

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

Google+ photo

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

Connecting to %s