Vamos aprender XNA? – Parte 13 – Aplicando texturas a uma malha gerada com Heightmap

Publicado em 05/08/2011

4


Olá pessoal, tudo certo?

O post de hoje está fortemente vinculado ao de ontem. De forma simples, vamos sair dessa renderização …

image

… para esta …

image

Como faremos isso?

  1. mudaremos a cor de fundo;
  2. aplicaremos uma textura na malha correspondente ao terreno.

Na aplicação da textura, voltamos a utilizar HLSL. Para não me tornar repetitvo, emito alguns detalhes ou conceitos passados em posts anteriores. Entretanto, em caso de dúvidas, estamos aí Smiley de boca aberta

Lembre-se que você pode consultar todos os posts dessa série. Além disso, pode pegar todos os fontes em https://github.com/ElemarJR/VamosAprenderXNA

Se quiser, você também pode me encontrar no Twitter:

Como mudei a cor de fundo?!

Mudar a cor de fundo é muito simples. Basta alterar o método Draw da classe Game para algo semelhante ao que segue:

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);
	//.. 
}

E ficamos assim:

image

Bacana, não!?

Escolhendo uma textura para aplicar na malha

Já temos nosso “céu azul”. Agora, precisamos escolher uma textura para aplicar a malha.

Geralmente, fotos aproximadas do tipo “revestimento” que desejamos aplicar, sem marcas muito fortes, são boas candidatas. Para nossas montanhas, selecionei essa textura aqui:

image

Escolhi essa textura com “irregularidades” para destacar as imperfeições de nossa montanha. Alegre

De VertexPositionColor para VertexPositionNormalBuffer

Ontem, usamos VertexPositionColor por ser muito simples. Entretanto, para podermos usar texturas precisamos usar outra estrutura: VertexPositionNormalTexture.

VertexPositionNormalTexture exige algumas informações a mais. Para ser mais preciso, precisamos calcular as coordenadas UV e o vetor Normal.

Veja a nova versão de CreateVertices:

void CreateVertices()
{
    this.Vertices = new VertexPositionNormalTexture[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++)
        {
            var position = new Vector3(
                x * this.CellSize,
                Heights[x, z],
                z * CellSize
                ) - offset;

            var uv = new Vector2((float)x / Width, (float)z / Length);

            this.Vertices[z * Width + x] = 
                new VertexPositionNormalTexture(
                    position,
                    Vector3.Zero,
                    uv
                    );
        }
}

Na prática, estou “esticando a textura” para que ela cubra toda a malha.

Repare que estou optando por não calcular o vetor normal dos vértices. Isso porque o vetor normal de cada vértice será uma combinação das normais todos os triângulos em que o vértice participa.

Calculando as normais para cada vértice

Calcular as normais para cada vértice não é um processo tão complicado quando possa parecer inicialmente. Observe:

void ComputeNormals()
{
    for (int i = 0; i < IndexCount; i += 3)
    {
        var v1 = Vertices[Indexes[i]].Position;
        var v2 = Vertices[Indexes[i + 1]].Position;
        var v3 = Vertices[Indexes[i + 2]].Position;

        var normal = Vector3.Cross(
            v1 - v2,
            v1 - v3
            );

        normal.Normalize();

        Vertices[Indexes[i]].Normal += normal;
        Vertices[Indexes[i + 1]].Normal += normal;
        Vertices[Indexes[i + 2]].Normal += normal;
    }

    for (int i = 0; i < VerticesCount; i++)
        Vertices[i].Normal.Normalize();
}

Calculo a normal de todos os triângulos da malha e acumulo o resultado em cada vértice envolvido. Depois, percorro novamente a lista de vértices normalizando o resultado.

Esse método é evocado no contrutor da classe Terrain (apresentado no post anterior).

Escrevendo um Effect para aplicação da textura

Agora que temos todas as informações para nossa malha utilizar texturas, vamos escrever o Effect para aplicação. (Arquivo BasicTerrainEffect.fx)

Começamos assim:

float4x4 World;
float4x4 View;
float4x4 Projection;

float3 LightDirection = float3(1, -1, 0);

float TextureTiling = 6;
texture2D Texture;
sampler2D TextureSampler = sampler_state {
	Texture = <Texture>;
	AddressU = Wrap;
	AddressV = Wrap;
	MinFilter = Anisotropic;
	MagFilter = Anisotropic;
};

Considere:

  • World, View e Projection são parâmetros comuns que dispensam comentários.
  • Como quero aplicar um sombreamento muito simples, baseado em uma luz direcional (imitando o sol), solicito apenas a direção da luz (Assumo 1,-1,0 como default).
  • TextureTiling diz respeito a quantas vezes “repetirei” a textura no terreno. Por default, nosso mapeamento considera que a textura inteira será aplicada uma única vez. Com esse parâmetro, estou dando esse controle ao código cliente. Se nada for informado, faço 6 repetições, em cada direção da textura (você verá como isso é simples no Pixel Shader que mostro a seguir).
  • No mapeamento da textura, declaro que caso ocorra uma coordenada UV que saia dos limites (0,1), devo recomeçar. Além disso, especifico que o sampler deverá suavizar (com uma espécie de blur) a textura.

Vamos seguir em frente, vamos analisar as estruturas de dados do Effect:

struct VertexShaderInput
{
    float4 Position : POSITION0;
	float2 UV : TEXCOORD0;
	float3 Normal : NORMAL0;
};

struct VertexShaderOutput
{
    float4 Position : POSITION0;
	float2 UV : TEXCOORD0;
	float3 Normal : TEXCOORD1;
};

Nada demais aqui também. Apenas estou alertando que desejarei utilizar as coordenadas UV e vetor Normal correspondentes ao vértice. O VertexShader também não tem nada de especial:

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output;

    float4 worldPosition = mul(input.Position, World);
    float4 viewPosition = mul(worldPosition, View);
    output.Position = mul(viewPosition, Projection);

	output.Normal = input.Normal;
	output.UV = input.UV;

    return output;
}

A mágina acontece no PixelShader:

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
	float light = dot(
		normalize(input.Normal),
		normalize(LightDirection)
		);
	
	light = clamp(light + 0.4f, 0, 1);

	float3 tex = tex2D(TextureSampler, input.UV * TextureTiling);

	return float4(tex * light, 1);
}

Crio um sombreamento direcional simples (já falamos sobre isso nos posts passados) e pego a textura considerando o TextureTiling (sim, é só uma multiplicação)

E, era isso

Usando o Effect na classe Terrain

Já temos nosso Effect pronto. Para utilizar, precisamos carregar o mesmo no método LoadContent do Game e “informar” a classe Terrain. Repare como passo o effect como “novo” parâmetro do construtor de Terrain:

var effect = Content.Load<Effect>("BasicTerrainEffect");
effect.Parameters["Texture"].SetValue(Content.Load<Texture2D>("grass"));

ground = new Terrain(
    Content.Load<Texture2D>("heightmap1"),
    effect,
    30, 4800, device
    );

Lindo!

Obviamente, o construtor de terrain salva esse effect em um atributo (no post anterior, usava um BasicEffect) para que seja utilizado na rotina de Draw.

Por fim, basta alterar a rotina Draw do Terrain. Observe:

public void Draw(Matrix View, Matrix Projection)
{
    Device.SetVertexBuffer(VertexBuffer);
    Device.Indices = IndexBuffer;

    Effect.Parameters["World"].SetValue(Matrix.Identity);
    Effect.Parameters["View"].SetValue(View);
    Effect.Parameters["Projection"].SetValue(Projection);

    foreach (var pass in Effect.CurrentTechnique.Passes)
    {
        pass.Apply();
        Device.DrawIndexedPrimitives(
            PrimitiveType.TriangleList,
            0, 0,
            VerticesCount, 0,
            IndexCount / 3);
    }
}

Lindo! Executando…

image

Por hoje, era isso

Alegre

Etiquetado:, ,
Publicado em: Sem categoria