Vamos aprender XNA? – Parte 14 – Billboard e FreeCamera

Publicado em 06/08/2011

0


Olá pessoal, como estamos?!

Nesse post, apresento os fundamentos para construção de um Billboard. Além disso, apresento como contruir uma câmera (livre) controlada por Mouse e Teclado.

Como sempre, você pode pegar todo o código-fonte em https://github.com/ElemarJR/VamosAprenderXNA

O que é um billboard?!

Billboard é uma técnica onde texturas 2D são desenhadas em retângulos 3D. Em muitos casos (como o que apresento hoje), esses retângulos são rotacionados para ficar “de frente” para a câmera.

No post de hoje, construiremos um billboard semelhante a esse:

image

Billboards são extremamente interessantes pois, em muitos casos, dispensam o desenho de modelos 3D mais complexos. Isso pode representar um ganho de performance excelente.

BillboardSystem

A técnica, em sí é muito simples. Observe a classe que desenvolvi para representar o Billboard.

class BillboardSystem
{
    public GraphicsDevice Device { get; private set; }
    public Texture2D Texture { get; private set; }
    int BillboardCount = 0;
     
    IndexBuffer IBuffer;
    VertexBuffer VBuffer;

    Effect BillboardEffect;

    public BillboardSystem(
        GraphicsDevice device,
        ContentManager content,
        Texture2D texture,
        Vector2 billboardSize,
        Vector3[] positions
        )
    {
        this.BillboardCount = positions.Length;
        this.Device = device;
        this.Texture = texture;
        //this.BillboardSize = billboardSize;
        this.CreateBillboards(positions);

        this.BillboardEffect = content.Load<Effect>
            ("BillboardEffect");

        BillboardEffect.Parameters["Texture"].SetValue(texture);
        BillboardEffect.Parameters["Size"].SetValue(billboardSize);
    }

    void CreateBillboards(Vector3[] positions)
    {
        var indices = new int[positions.Length * 6];
        var vertices = new VertexPositionTexture[positions.Length * 4];
        int j = 0;

        for (int i = 0; i < positions.Length * 4; i+=4)
        {
            var pos = positions[i / 4];
            vertices[i + 0] = new VertexPositionTexture(pos, Vector2.Zero);
            vertices[i + 1] = new VertexPositionTexture(pos, new Vector2(0, 1));
            vertices[i + 2] = new VertexPositionTexture(pos, new Vector2(1, 1));
            vertices[i + 3] = new VertexPositionTexture(pos, new Vector2(1, 0));

            indices[j++] = i + 0;
            indices[j++] = i + 3;
            indices[j++] = i + 2;
            indices[j++] = i + 2;
            indices[j++] = i + 1;
            indices[j++] = i + 0;
        }
            
        VBuffer = new VertexBuffer(
            Device,
            typeof(VertexPositionTexture),
            positions.Length * 4,
            BufferUsage.WriteOnly);

        VBuffer.SetData<VertexPositionTexture>(vertices);

        IBuffer = new IndexBuffer(
            Device,
            IndexElementSize.ThirtyTwoBits,
            positions.Length * 6,
            BufferUsage.WriteOnly);

        IBuffer.SetData<int>(indices);
    }

    public void Draw(Matrix view, Matrix projection, Vector3 up, Vector3 right)
    {
        BillboardEffect.Parameters["View"].SetValue(view);
        BillboardEffect.Parameters["Projection"].SetValue(projection);
        BillboardEffect.Parameters["CameraUp"].SetValue(up);
        BillboardEffect.Parameters["CameraSide"].SetValue(right);

        BillboardEffect.CurrentTechnique.Passes[0].Apply();

        Device.SetVertexBuffer(this.VBuffer);
        Device.Indices = this.IBuffer;

        Device.DrawIndexedPrimitives(
            PrimitiveType.TriangleList,
            0, 0,
            BillboardCount * 4,
            0,
            BillboardCount * 2
            );

        Device.SetVertexBuffer(null);
        Device.Indices = null;
    }
}

Vamos falar um pouco de CreateBillboard. Repare como o método cria uma coleção de retângulos no ambiente. Como já sabemos, cada retângulo, em XNA, é sempre formado por dois triângulos.

O método Draw, basicamente, desenha os triângulos calculados por CreateBillboard passando informações requisitadas pelo Effect criado para renderizar o efeito.

BillboardEffect

Como podemos perceber, o “trabalho” duro para nosso billboard é todo feito no effect. Vamos analisar em partes. Comecemos pelos parâmetros e estruturas:

float4x4 View;
float4x4 Projection;

texture Texture;
sampler2D TextureSampler = sampler_state {
	texture = <Texture>;
};

float2 Size;
float3 CameraUp; 
float3 CameraSide; 

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

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

Os principais elementos aqui são:

  • Size – Tamanho da textura;
  • CameraUp – Vetor Up para a câmera;
  • CameraSide – Vetor a partir da câmera, apontando para a direita;

Agora, o Vertex shader:

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
	VertexShaderOutput output;

	float3 position = input.Position;

	float2 offset = float2(
		(input.UV.x - 0.5f) * 2.0f, 
		-(input.UV.y - 0.5f) * 2.0f
	);

	position += offset.x * Size.x * CameraSide + offset.y * Size.y * CameraUp;

	output.Position = mul(float4(position, 1), mul(View, Projection));

	output.UV = input.UV;

	return output;
}

O cálculo do offset serve para que possamos executar nossas transformações a partir do centro.

Por fim, temos o Pixel Shader:

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
	float4 color = tex2D(TextureSampler, input.UV);
	clip(color.a - 0.5 * 1);

	return color;
}

Basicamente, pegamos a cor do pixel na textura. Com a função clip, orientamos o HLSL a descartar esse pixel caso o alfa (transparência) esteja acima de 0.5.

Carregando nossa “mini-floresta”

Já temos nosso sistema para desenhar billboards, agora vamos carregar nosssa floresta. Observe:

private void CreateBillboardSystem()
{
    var r = new Random();
    var positions = new Vector3[150];
    var tree = Content.Load<Texture2D>("tree");

    for (int i = 0; i < positions.Length; i++)
    {
        var x = (float)(r.NextDouble() - 0.5) * 20000;
        var y = (float)(r.NextDouble() - 0.5) * 20000;
        positions[i] = new Vector3(x, tree.Bounds.Height, y);
    }

    trees = new BillboardSystem(GraphicsDevice, Content,
        tree, new Vector2(tree.Bounds.Width, tree.Bounds.Height),
        positions);
}

Perceba:

  • definimos que nosso ambiente terá 150 árvores;
  • carregamos a imagem usando a content pipe line;
  • distribuí as árvores aleatóriamente em todo o terreno (que tem 20000 x 20000);

FreeCamera

Até aqui, nesse tutorial, sempre usei em nossos exemplos uma câmera que “perseguia” um modelo 3D.

Para o post de hoje, escrevi uma câmera mais simples. Observe:

public class FreeCamera : Camera
{
    public float Yaw { get; set; }
    public float Pitch { get; set; }
    public Vector3 Position { get; set; }
    public Vector3 Target { get; private set; }
    public Vector3 Up { get; private set; }
    public Vector3 Right { get; private set; }


    public FreeCamera(
        Vector3 position,
        float yaw,
        float pitch,
        GraphicsDevice graphicsDevice)
        : base(graphicsDevice)
    {
        this.Position = position;
        this.Yaw = yaw;
        this.Pitch = pitch;
    }

    public void Rotate(float yawChange, float pitchChange)
    {
        this.Yaw += yawChange;
        this.Pitch += pitchChange;
    }

    public void Move(Vector3 translation)
    {
        Matrix rotation = Matrix.CreateFromYawPitchRoll(Yaw, Pitch, 0);
        translation = Vector3.Transform(translation, rotation);
        Position += translation;
        translation = Vector3.Zero;
    }

    public override void Update()
    {
        Matrix rotation = Matrix.CreateFromYawPitchRoll(Yaw, Pitch, 0);

        Vector3 forward = Vector3.Transform(Vector3.Forward, rotation);
        Target = Position + forward;

        Vector3 up = Vector3.Transform(Vector3.Up, rotation);
        this.Up = up;
        this.Right = Vector3.Cross(forward, Up);

        View = Matrix.CreateLookAt(Position, Target, up);
    }
}

Repare que controlo a posição da câmera, em Position, e a direção da câmera com Yaw e Pitch. Se você não está familiarizado com esses termos, observe:

Os vetores na figura (encontrada na Wikipedia) indicam os eixos de rotação.

Perceba como calculo o vetor para a direita através do produto do vetor “para frente” com o vetor “para cima”

Movendo a câmera

Agora que temos como representar nossa câmera, gostaria de mostrar uma alternativa para mudar sua posição e rotação. Eis a abordagem que estou utilizando (bem comum e difundida):

protected override void Update(GameTime gameTime)
{
    MouseState mouseState = Mouse.GetState();
    KeyboardState keyState = Keyboard.GetState();

    float deltaX = (float)lastMouseState.X - (float)mouseState.X;
    float deltaY = (float)lastMouseState.Y - (float)mouseState.Y;
    ((FreeCamera)camera).Rotate(deltaX * .005f, deltaY * .005f);

    Vector3 translation = Vector3.Zero;
    if (keyState.IsKeyDown(Keys.Up)) 
        translation += Vector3.Forward;
    if (keyState.IsKeyDown(Keys.Down)) 
        translation += Vector3.Backward;
    if (keyState.IsKeyDown(Keys.Left)) 
        translation += Vector3.Left;
    if (keyState.IsKeyDown(Keys.Right)) 
        translation += Vector3.Right;

    translation *= 4 *
        (float)gameTime.ElapsedGameTime.TotalMilliseconds;
                
    ((FreeCamera)camera).Move(translation);

    camera.Update();

    lastMouseState = mouseState;

    base.Update(gameTime);
}

Basicamente, estou utilizando os movimentos do mouse para mudar a rotação da câmera, e as “setinhas” para mudar a posição.

Pronto! Era isso!

Smiley piscando

Etiquetado:, ,
Publicado em: Sem categoria