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:
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!
![]()






Publicado em 06/08/2011
0