Olá pessoal, como estamos!?
Passados uns dias de “DLR”, volto a escrever um pouco sobre XNA.
No post de hoje, deixo um pouco de lado HLSL e volto a falar de algoritmo. Minha proposta: substituir o já habitual “quadriculado” que estou usando de “chão”, por um terreno (irregular). O resultado é mais ou menos esse:
Repare que optei por não utilizar texturas para o terreno. Isso fica para outros posts.
Para gerar esse terreno, utilizei uma técnica bacana: geração de terreno através de heightmaps.
Se você está chegando agora e não entende muito de desenho 3D no XNA, de uma olhada na parte 4.
Recomendo fortemente que você pegue os fontes em https://github.com/ElemarJR/VamosAprenderXNA.
O que são HeightMaps?
HeightMaps são texturas, geralmente em tons de cinza usadas como referência para geração de uma malha simulando terrenos. É muito útil para construção de espaços “acidentados”.
Para uma idéia, o terreno acima orgina-se desse mapa:
Na lógica que adotei, quanto mais claro (próximo de branco) for o ponto, mais alto será a posição correspondnete no terreno.
A classe Terrain
Isolei a lógica para criação do terreno a partir de um heightmap em uma única classe chamada Terrain. Observe a assinatura básica dos métodos e atributos:
public class Terrain : IDrawableModel
{
public Terrain(Texture2D heightMap,
float cellSize,
int maxHeight,
GraphicsDevice device)
{
// ...
}
void ComputeHeights()
{
// ...
}
void CreateVertices()
{
// ...
}
void CreateIndices()
{
// ...
}
float[,] Heights;
int[] Indexes;
public readonly Texture2D HeightMap;
public readonly int Width;
public readonly int Height;
public readonly int Length;
public readonly float CellSize;
readonly int VerticesCount;
VertexPositionColor[] Vertices;
readonly int IndexCount;
readonly VertexBuffer VertexBuffer;
readonly IndexBuffer IndexBuffer;
GraphicsDevice Device;
BasicEffect Effect;
public void Draw(Matrix View, Matrix Projection)
{
// ...
}
}
Repare como a interface pública da classe é reduzida. Há apenas um construtor e o método de desenho.
O construtor é “acionado” no LoadContent do game. O método Draw, no Draw do game.
Todo “cálculo pesado” é executado por métodos privados que são acionados no construtor.
O construtor do terreno
Antes de falar do código do construtor, vamos ver ele sendo acionado:
ground = new Terrain(
Content.Load<Texture2D>("heightmap1"),
30, 4800, device
);
Vamos falar um poquinho sobre os parâmetros:
- heightmap – a textura com indicação das elevações que desejamos ver no terreno. Repare que estou usando a Content Pipeline aqui para carregar esse conteúdo;
- cellSize – o tamanho de cada célula. Na prática, haverá uma “célula” (quadradinho) para cada pixel da textura. No nosso exemplo, estaremos assumindo que cada célula terá dimensão 30 para cada pixel;
- maxHeight – altura máxima para o terreno. No exemplo, estou orientando que o pixel totalmente branco equivale a 4800 de altura;
- device – device onde o desenho deverá ser realizado.
Agora, o código do construtor:
public Terrain(Texture2D heightMap,
float cellSize,
int maxHeight,
GraphicsDevice device)
{
rs.FillMode = FillMode.WireFrame;
this.HeightMap = heightMap;
this.Width = heightMap.Width;
this.Height = maxHeight;
this.Length = heightMap.Height;
this.CellSize = cellSize;
this.Device = device;
this.Effect = new BasicEffect(device);
this.VerticesCount = Width * Length;
this.IndexCount = (Width - 1) * (Length - 1) * 6;
this.VertexBuffer = new VertexBuffer(device,
typeof(VertexPositionColor), VerticesCount,
BufferUsage.WriteOnly);
this.IndexBuffer = new IndexBuffer(device,
IndexElementSize.ThirtyTwoBits,
IndexCount, BufferUsage.WriteOnly);
ComputeHeights();
CreateVertices();
CreateIndices();
this.VertexBuffer.SetData<VertexPositionColor>(Vertices);
this.IndexBuffer.SetData<int>(Indexes);
}
Repare que nosso construtor simplesmente atribui valores para os “atributos” do terreno. Além disso, ele invoca os métodos que geram a malha.
Calculando alturas (método ComputeHeights)
Antes de gerar o terreno, calculo as alturas equivalentes a cada pixel. Observe o código:
void ComputeHeights()
{
var heightMapColors = new Color[Width * Length];
HeightMap.GetData<Color>(heightMapColors);
Heights = new float[Width, Length];
for (int y = 0; y < Length; y++)
for (int x = 0; x < Width; x++)
Heights[x, y] = heightMapColors[y * Width + x].R / 255.0f * Height;
}
Lindo, não! Basicamente, varro cada pixel da textura e calculo a altura. Observe que:
- utilizo apenas o componente vermelho da cor, isso é possível porque em imagens em “tons de cinza” todas os componentes tem o mesmo valor;
- divido o valor de cada pixel por 255. Isso gera um peso entre 0 e 1;
- multiplico o componente da altura pela “altura máxima” passada no construtor.
Calculando vértices (método CreateVertices)
Já conhecemos as “alturas” da textura. Agora, vamos computar os vértices da malha. Observe:
void CreateVertices()
{
this.Vertices = new VertexPositionColor[VerticesCount];
Vector3 offset = new Vector3(
(Width / 2f) * CellSize, 0,
(Length / 2f) * CellSize
);
for (int z = 0; z < Length; z++)
for (int x = 0; x < Width; x++)
{
Vector3 position = new Vector3(
x * this.CellSize,
Heights[x, z],
z * CellSize
) - offset;
this.Vertices[z * Width + x] = new VertexPositionColor(position, Color.Green);
}
}
Para começar, repare que computo um offset para garantir que o centro da malha esteja no ponto 0.
Perceba que a utilização de CellSize “escala” a textura.
Criando índices (método CreateIndices)
Já temos os vértices, agora temos que calcular índices para a malha.
Repare que, para cada célula, computamos dois triângulos:
Eis o cálculo:
void CreateIndices()
{
this.Indexes = new int[this.IndexCount];
int index = 0;
for (int x = 0; x < Width - 1; x++)
for (int z = 0; z < Length - 1; z++)
{
int ul = z * Width + x;
int ur = ul + 1;
int ll = ul + Width;
int lr = ll + 1;
Indexes[index++] = ul;
Indexes[index++] = ur;
Indexes[index++] = ll;
Indexes[index++] = ll;
Indexes[index++] = ur;
Indexes[index++] = lr;
}
}
Lindo demais!
Desenhando.. (método Draw)
Tudo calculado. Problema resolvido. Agora é só desenhar. Observe:
public void Draw(Matrix View, Matrix Projection)
{
Device.SetVertexBuffer(VertexBuffer);
Device.Indices = IndexBuffer;
var old = Device.RasterizerState;
Device.RasterizerState = rs;
Effect.World = Matrix.Identity;
Effect.View = View;
Effect.Projection = Projection;
Effect.TextureEnabled = false;
Effect.VertexColorEnabled = false;
foreach (var pass in Effect.CurrentTechnique.Passes)
{
pass.Apply();
Device.DrawIndexedPrimitives(
PrimitiveType.TriangleList,
0, 0,
VerticesCount, 0,
IndexCount / 3);
}
Device.RasterizerState = old;
}
O método, em si, não apresenta grandes novidades. O único ponto aqui é o “set” do RasterizeState para wireframe.
Por hoje, temos isso:
Outro dia, vamos colocar um pouco de texturas nesse terreno.
Follow @elemarjr





AlexandreRDSST
15/01/2012
E ae blz
To estudando XNA sei programa c# mas tenho uma duvida eu garimpei a net toda e naum axei nada a respeito sobre, fazer um personagem andar num terreno, desliza sobre as meches de um modelo 3d modelado no blender por exemplo
poderia me da uma luz de como é feito isso? como fazer o personagem desliza sobre um terreno acompanhando a elevação das montanhas e tal?
elemarjr
16/01/2012
Olá!
Não há mágica a fazer. No fim, precisará calcular a posição na unha. Em breve, devo mostrar um exemplo assim.
[]s