Olá pessoal, tudo certo?
Até aqui, em todos os posts anteriores, trabalhamos apenas em 2D. Hoje vamos começar a explorar as possibilidades 3D do XNA.
Lembre-se que o código fonte está disponível em https://github.com/ElemarJR/VamosAprenderXNA
Sem mais delongas…
Sistema de coordenadas 2D do XNA
Em XNA, trabalhar com 2D assemelha-se a trabalhar com um canvas padrão. Convencionou-se que:
- cada posição da tela está associada a uma coordenada;
- Coordenadas 2D são pares ordenados (X, Y);
- o canto superior esquerdo da área de jogo corresponde a coordenada (0, 0);
- X cresce para a direita;
- Y cresce para baixo;
- em xna, usa-se a struct Vector2 para representar uma coordenada 2D.
Se você já teve a necessidade de posicionar controles em um formulário, por código, entende perfeitamente o sistema de coordenadas 2D do XNA.
Sistemas de coordenadas 3D
Trabalhar com 3D exige um pouco mais de abstração. Considere:
- uma coordenada 3D tem três elementos, (X, Y, Z);
- um sistema de coordenadas 3D também pode ser chamado de world space;
- todo world space contém um ponto especial, chamado origem, que corresponde a coordenada (0, 0, 0);
- assume-se que o ponto origem corresponda ao “centro” do world space;
- mesmo sendo o “centro” do world space, a origem não corresponde, necessariamente, ao centro da área de jogo.
Coordenadas 3D lembram aquilo que aprendemos em sala de aula. Ou seja,
- X cresce para direita;
- Y cresce para cima;
- Z pode crescer para trás (para dentro da tela) ou para a frente (para fora da tela).
Sistemas onde Z cresce para “trás” (dentro da tela) são chamados de left-handed. Quando cresce para frente (fora da tela), são chamados right-handed. Observe na figura o porquê:
A dinâmica consiste em colocar as mãos, com de palmas para cima com os dedos apontando para X (direita) curvados para Y (cima). A direção “natural” do polegar corresponde a direção de crescimento de Z.
XNA usa o sistema right-handed.
Câmera e sua relação com o “mundo”
O que se vê na tela, na visualização de um ambiente 3D, depende de dois componentes básicos:
- os objetos inseridos, e suas posições, no “mundo”;
- a posição e direção da câmera;
Apenas objetos “capturados” pela câmera são visiveis na tela.
Dependendo de onde a câmera esteja posicionada e de sua direção, um objeto que você desenhar na origem podera estar no centro da tela, ou mais abaixo, ou em qualquer outro lugar. Por isso, fiz a analogia de “visualização 3D” como sendo a gravação de um filme usando uma câmera de mão.
Em computação gráfica, uma câmera possui diversas propriedades: localização, direção da lente, abertura. Em XNA, todas essas propriedades ficam armazenadas em objetos do tipo Matrix (sim, as matrizes).
Matrizes são entidades matemáticas, relativamente complexas, cujo explicação está fora do escopo desse post (ou série). Entretanto, é necessário deixar claro que elas estão relacionadas com praticamente tudo que se possa fazer com 3D (Se você não sabe usar, melhor aprender).
XNA oculta quase toda a complexidade relacionada com a operação de matrizes. Por isso, mesmo que você não saiba como elas funcionam, pode continuar lendo.
Para representar a câmera em XNA usamos duas matrizes:
- view – armazena informação que determina onde a câmera está no “mundo”, para onde está “apontando” e sua orientação.
- projection – armazena, entre outras informações, o angulo de visão (abertura), distância mínima que um objeto precisa estar para ficar visível e a distância máxima. É essa matriz que guarda a informação necessária para converter a “visão 3D” em algo 2D que possa ser desenhado na tela.
A matriz view é criada pelo método estático CreateLookAt da classe Matrix. A matriz projection é criada pelo método CreatePerspectiveFieldOfView.
Por questão de organização, recomendo colocar as duas instâncias de Matrix correspondentes a câmera em um tipo. Observe:
public class Camera : Microsoft.Xna.Framework.GameComponent { public Matrix View { get; private set; } public Matrix Projection { get; private set; } public Camera(Game game, Vector3 position): this (game, position, Vector3.Zero) {} public Camera(Game game, Vector3 position, Vector3 target) : this(game, position, target, Vector3.Up) {} public Camera(Game game, Vector3 position, Vector3 target, Vector3 up) : base(game) { var ratio = (float) Game.Window.ClientBounds.Width / (float) Game.Window.ClientBounds.Height; View = Matrix.CreateLookAt(position, target, up); Projection = Matrix.CreatePerspectiveFieldOfView( MathHelper.PiOver4, ratio, 1, 100 ); } public override void Initialize() { base.Initialize(); } public override void Update(GameTime gameTime) { base.Update(gameTime); } }
Cabem alguns comentários:
- Criei a classe Camera herdando de GameComponent. Um GameComponent pode ser registrada na classe Game e ter seu método Update evocado automaticamente;
- Criei sobrecargas do construtor para facilitar chamadas dos clientes;
- Dos três parâmetros de CreateLookAt, apenas up precisa ser explicado. Ele serve para dizer qual dos eixos aponta “para cima” no sistema de coordenadas. Por hora, assuma que geralmente esse vetor corresponde a(0,1,0);
- O método CreatePerspectiveFieldOfView recebe quatro parâmetros:
-
- ângulo de abertura da câmera (FoV);
- razão da projeção (proporção entre largura e altura da viewport)
- distância mínima que uma primitiva deve estar para ser visível pela câmera (ZNear);
- distância máxima que uma primitiva deve estar para ser visível pela câmera (ZFar).
- XNA opera com radianos. Se você matou as aulas de trigonometria, lembre-se que PI = 180. Logo, a constante PiOver4 (PI sobre 4), corresponde a 45 graus.
Acredito que a imagem a seguir ajuda a ilustrar a idéia (peguei daqui)
Para integrar a “câmera” no jogo, crio um atribuo na classe Game e “inicio” a câmera em LoadContent. Observe:
public class Game1 : Microsoft.Xna.Framework.Game { //.. Camera camera; // .. protected override void LoadContent() { // camera camera = new Camera(this, new Vector3(0, 0, 5)); Components.Add(camera); } }
Por questões de espaço, mantive apenas o código correspondente a configração da câmera no Game. Executando:
Temos uma tela vazia! Confiuramos uma câmera, mas não colocamos nenhum objeto no mundo.
Para tornar claro: temos uma câmera, posicionada em (0,0,5) apontando para (0,0,0).
Desenhando nossa primeira primitiva 3D
Agora que temos uma câmera configurada, precisamos desenhar alguma coisa na tela.
A base de todo desenho em 3D são triângulos. A idéia é: Se desenharmos triângulos em número suficiente, podemos renderizar qualquer coisa!
Em XNA, para desenharmos um triângulo, precisamos de uma lista de pontos, ou vértices. Também precisamos de um objeto do tipo VertexBuffer no qual armazenamos algumas informações sobre os vértices que serão enviadas diretamente para o adaptador de vídeo.
O Game a seguir desenha um triângulo na tela:
public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; Camera camera; VertexPositionColor[] verts; VertexBuffer vertexBuffer; BasicEffect effect; public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; } protected override void Initialize() { base.Initialize(); } protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); // camera camera = new Camera(this, new Vector3(0, 0, 5)); Components.Add(camera); // colors verts = new[] { new VertexPositionColor(new Vector3(0, 1, 0), Color.Blue), new VertexPositionColor(new Vector3(1, -1, 0), Color.Red), new VertexPositionColor(new Vector3(-1, -1, 0), Color.Green) }; // vertex vertexBuffer = new VertexBuffer(GraphicsDevice, typeof(VertexPositionColor), verts.Length, BufferUsage.None); vertexBuffer.SetData(verts); // effect effect = new BasicEffect(GraphicsDevice); } protected override void UnloadContent() { } protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); base.Update(gameTime); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); GraphicsDevice.SetVertexBuffer(vertexBuffer); effect.World = Matrix.Identity; effect.View = camera.View; effect.Projection = camera.Projection; effect.VertexColorEnabled = true; foreach (var pass in effect.CurrentTechnique.Passes) { pass.Apply(); GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleStrip, verts, 0, 1); } base.Draw(gameTime); } }
Quando executado, resulta em:
Bonito. Explicando…
Um objeto VertexPositionColor representa um vértice com informações relacionadas a posição e a cor. No nosso exemplo, criei um vetor simples com três vértices. Observe que criei o triângulo em torno da origem.
verts = new[] { new VertexPositionColor(new Vector3(0, 1, 0), Color.Blue), new VertexPositionColor(new Vector3(1, -1, 0), Color.Red), new VertexPositionColor(new Vector3(-1, -1, 0), Color.Green) };
Para XNA conseguir trabalhar preciso criar um buffer, ajustado para a “GraphicsDevice” disponível. Para isso, criamos uma instância de VertexBuffer e “setamos” os vértices recém criados. Observe:
vertexBuffer = new VertexBuffer(GraphicsDevice, typeof(VertexPositionColor), verts.Length, BufferUsage.None); vertexBuffer.SetData(verts);
Para, finalmente, fazer o desenho na tela, alteramos o método Draw. Observe:
protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); GraphicsDevice.SetVertexBuffer(vertexBuffer); effect.World = Matrix.Identity; effect.View = camera.View; effect.Projection = camera.Projection; effect.VertexColorEnabled = true; foreach (var pass in effect.CurrentTechnique.Passes) { pass.Apply(); GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleStrip, verts, 0, 1); } base.Draw(gameTime); }
Começamos enviando nosso buffer de vértices para a placa de vídeo. O objeto effect é nosso operário 3D, é ele que operacionaliza o “desenho” na tela.
Como pode ser visto na imagem gerada por nosso “game”, cada vértice foi “desenhado” na posição que foi especificada. Além disso, cada vértice foi desenhado na cor especificada. Perceba como todo o restante do triângulo é preenchido com uma mistura entre as cores dos vértices, predominando sempre a cor do vértice mais próximo.
XNA e HLSL
Em baixo nível, tudo no XNA é desenhado usando um programa escrito em uma linguagem chamada HLSL (High Level Shader Language). Trata-se de uma linguagem, baseada em C, que permite a desenvolvedores escrever poderosos efeitos gráficos como sombras, reflexos e movimento.
Um programa XNA acessa uma rotina HLSL usando um elemento chamado Effect. A classe Effect permite que um programa em C# mande dados para a rotina em HLSL.
O time do XNA desenvolveu uma rotina padrão HLSL para desenho de primitivas em XNA. Esta rotina está acessível através da classe BasicEffect. Ela fornece suporte básico e rotinas simples para renderização 3D usando HLSL.
Para o post de hoje, usaremos apenas BasicEffect em nossas renderizações.
Observe, no exemplo anterior, que a criação de BasicEffect deve ocorrer no método LoadContent. Enquanto sua configuração ocorre repetidamente no método Draw. Observe:
effect.World = Matrix.Identity; effect.View = camera.View; effect.Projection = camera.Projection; effect.VertexColorEnabled = true;
A primeira propriedade setada é World. Essa propriedade representa a posição em que do mundo em que o objeto deverá ser desenhado. Como estamos querendo desenhar o objeto na origem, usamos uma matriz identidade.
As próximas propriedades setadas determinam as matrizes View e Projection para essa renderização. Observe que essas propriedades definem, para a Viewport, as propriedades da câmera.
Por fim, temos a propriedade VertexColorEnabled. Essa propriedade “avisa” a rotina HLSL que desejamos que seja calculada a cor de cada pixel conforme sua proximidade com os vértices.
Por fim, temos a aplicação das técnicas. Observe:
foreach (var pass in effect.CurrentTechnique.Passes) { pass.Apply(); GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleStrip, verts, 0, 1); }
Cada effect possui uma ou mais Techniques. Cada techinique tem um ou mais passes. O loop percorre todos os passes de todas techniques e desenha o triângulo. Deixemos o significado desses objetos para o post que discutiremos a criação de rotinas HLSL. Por hora, assumamos que essa etapa é necessária.
O método DrawUserPrimitives desenha triângulos a partir da lista de vértices fornecida.
Adicionando algum movimento – multiplicando matrizes
Certo! Conseguimos desenhar um triângulo bem colorido na tela. Mas acho que podemos esperar um pouco mais, não é mesmo?! Vamos falar um pouco sobre translação e rotação.
Como mencionei anteriormente, matrizes são a base de praticamente tudo que podemos fazer em cenas 3D. Isso é ainda mais verdadeiro para mover, rotacionar ou escalar um objeto.
Já sabemos que a propriedade World do objeto BasicEffect é a matriz que diz para o XNA onde desenhar um objeto no mundo, com que rotação e escala. (Realmente recomendo que você estude um pouco mais sobre matrizes se deseja trabalhar com 3D).
Para nossos propósitos, basta saber que translação, escala e rotação em gráficos 3D ocorre por meio de multiplicação de matrizes. A matriz representa por Matrix.Identity é o que identificamos como matriz identidade: uma matriz especial que, quando envolvida em uma multiplicação, se comporta como o número um. Ou seja, o produto da multiplicação de qualquer matriz pela matriz identidade é igual a ela mesma.
O Game que mostro agora continua desenhando apenas um triângulo colorido na tela. Entretanto, permite que o usuário desloque esse triângulo para a esquerda e para a direita. Observe:
public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; Camera camera; Matrix worldTranslation = Matrix.Identity; VertexPositionColor[] verts; VertexBuffer vertexBuffer; BasicEffect effect; public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; } protected override void Initialize() { base.Initialize(); } protected override void LoadContent() { // Create a new SpriteBatch, which can be used to draw textures. spriteBatch = new SpriteBatch(GraphicsDevice); // camera camera = new Camera(this, new Vector3(0, 0, 5)); Components.Add(camera); // colors verts = new[] { new VertexPositionColor(new Vector3(0, 1, 0), Color.Blue), new VertexPositionColor(new Vector3(1, -1, 0), Color.Red), new VertexPositionColor(new Vector3(-1, -1, 0), Color.Green) }; vertexBuffer = new VertexBuffer(GraphicsDevice, typeof(VertexPositionColor), verts.Length, BufferUsage.None); vertexBuffer.SetData(verts); // effect effect = new BasicEffect(GraphicsDevice); } protected override void UnloadContent() { } protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); KeyboardState keyboardState = Keyboard.GetState(); if (keyboardState.IsKeyDown(Keys.Left)) worldTranslation *= Matrix.CreateTranslation(-.01f, 0, 0); if (keyboardState.IsKeyDown(Keys.Right)) worldTranslation *= Matrix.CreateTranslation(.01f, 0, 0); if (keyboardState.IsKeyDown(Keys.Up)) worldTranslation *= Matrix.CreateTranslation(0, .01f, 0); if (keyboardState.IsKeyDown(Keys.Down)) worldTranslation *= Matrix.CreateTranslation(0, -.01f, 0); base.Update(gameTime); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); GraphicsDevice.SetVertexBuffer(vertexBuffer); effect.World = worldTranslation; effect.View = camera.View; effect.Projection = camera.Projection; effect.VertexColorEnabled = true; foreach (var pass in effect.CurrentTechnique.Passes) { pass.Apply(); GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleStrip, verts, 0, 1); } base.Draw(gameTime); } }
Se você compilar e executar esse Game, perceberá que precionando as setas verá o objeto se deslocando de acordo.
Mas, o que está ocorrendo?!
Basicamente, em cada interação do método Update, verificamos se uma seta está pressionada. Em caso positivo, gero uma matriz de translação correspondente e combino com a matriz presente no atributo worldTranslation.
Perceba que essa “combinação” ocorre por uma multiplicação.
Rotação e escala
Superado o desafio da translação. Resta-nos o desafio da rotação e da escala. Observe implementação desses recursos:
public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; Camera camera; Matrix worldTranslation = Matrix.Identity; Matrix worldRotation = Matrix.Identity; Matrix worldScale = Matrix.Identity; VertexPositionColor[] verts; VertexBuffer vertexBuffer; BasicEffect effect; public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; } protected override void Initialize() { base.Initialize(); } protected override void LoadContent() { // Create a new SpriteBatch, which can be used to draw textures. spriteBatch = new SpriteBatch(GraphicsDevice); // camera camera = new Camera(this, new Vector3(0, 0, 5)); Components.Add(camera); // colors verts = new[] { new VertexPositionColor(new Vector3(0, 1, 0), Color.Blue), new VertexPositionColor(new Vector3(1, -1, 0), Color.Red), new VertexPositionColor(new Vector3(-1, -1, 0), Color.Green) }; // vertex vertexBuffer = new VertexBuffer(GraphicsDevice, typeof(VertexPositionColor), verts.Length, BufferUsage.None); vertexBuffer.SetData(verts); // effect effect = new BasicEffect(GraphicsDevice); } protected override void UnloadContent() { } protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); KeyboardState keyboardState = Keyboard.GetState(); if (keyboardState.IsKeyDown(Keys.Left)) worldTranslation *= Matrix.CreateTranslation(-.01f, 0, 0); if (keyboardState.IsKeyDown(Keys.Right)) worldTranslation *= Matrix.CreateTranslation(.01f, 0, 0); if (keyboardState.IsKeyDown(Keys.Up)) worldTranslation *= Matrix.CreateTranslation(0, .01f, 0); if (keyboardState.IsKeyDown(Keys.Down)) worldTranslation *= Matrix.CreateTranslation(0, -.01f, 0); if (keyboardState.IsKeyDown(Keys.I)) worldScale *= Matrix.CreateScale(1.02f); if (keyboardState.IsKeyDown(Keys.O)) worldScale *= Matrix.CreateScale(0.98f); worldRotation *= Matrix.CreateRotationY(MathHelper.PiOver4 / 60); base.Update(gameTime); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); GraphicsDevice.SetVertexBuffer(vertexBuffer); effect.World = worldScale * worldRotation * worldTranslation; effect.View = camera.View; effect.Projection = camera.Projection; effect.VertexColorEnabled = true; foreach (var pass in effect.CurrentTechnique.Passes) { pass.Apply(); GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleStrip, verts, 0, 1); } base.Draw(gameTime); } }
Compilando e executando esse programa, percebemos que nosso triângulo fica “girando” direto. Além disso, podemos usar as teclas I e O para dar aproximar ou afastar o objeto. Não se assuste com o fato do triângulo sumir por um tempinho (é o backface culling, que explico em seguida).
A próxima imagem mostra um estágio de rotação:
A próxima imagem mostra um estágio de rotação para nosso triângulo aproximado (depois de pressionar algumas vezes a tecla I).
A lógica utilizada foi a mesma da translação. Observe que, para não acumular as operações, utilizei três atributos para representar as três matrizes (translação, rotação e escala). Mais uma vez: por favor, estude matemática de matrizes se pretende entender algo de programação 3D.
Backface culling
No nosso exemplo anterior, o triângulo simplesmente “some” durante um tempinho. Isso ocorre por causa de um processo chamado backface culling.
Culling é um processo em computação gráfica 3D que limita o número de coisas desenhadas na tela e melhora a performance. Essencialmente, seu objetivo é desenhar apenas o lado de uma primitiva que esteja direcionado para a câmera (vetor normal direcionado para a câmera). Logo, na nossa aplicação, apenas um lado do triângulo é desenhado.
Para entender o porquê do processo de culling melhorar a performance, imaginemos uma bola de futebol. Imaginemos estar escrevendo a próxima versão do Fifa soccer, ou do PES. Alguém se interessa pelo que há dentro da bola? Obviamente não. Então, seria perda de tempo fazer o XNA desenhar isso.
Assim como uma bola de futebol é oca, todos os objetos gráficos 3D que desenhamos são ocos. Todos eles são apenas uma espécie de “casca” desenhada em torno de um espaço vazio usando uma série de triângulos. O processo de culling impede que o lado de dentro da casca seja desenhado.
Para essa demonstração, entretanto, seria interessante desligar o processo de culling. Isso é possível! Basta adicionar as seguintes linhas no final de nosso método LoadContent:
RasterizerState rs = new RasterizerState(); rs.CullMode = CullMode.None; GraphicsDevice.RasterizerState = rs;
Vale observar que, geralmente, não desligamos o Culling. Isso teria implicações sérias para a performance.
Desenhando um retângulo – Primitive types
Desenhar um triângulo é bacana, mas, que tal desenhar um retângulo?!
Bem, a má notícia do dia é que não há ferramenta para desenhar diretamente um retângulo. Para fazer isso, temos que desenhar dois triângulos. Eis, nossa versão do Game para desenhar um retângulo (dois triângulos) coloridos.
public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; Camera camera; //Texture2D texture; Matrix worldTranslation = Matrix.Identity; Matrix worldRotation = Matrix.Identity; Matrix worldScale = Matrix.Identity; VertexPositionColor[] verts; VertexBuffer vertexBuffer; BasicEffect effect; public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; } protected override void Initialize() { base.Initialize(); } protected override void LoadContent() { // Create a new SpriteBatch, which can be used to draw textures. spriteBatch = new SpriteBatch(GraphicsDevice); RasterizerState rs = new RasterizerState(); rs.CullMode = CullMode.None; GraphicsDevice.RasterizerState = rs; // camera camera = new Camera(this, new Vector3(0, 0, 5)); Components.Add(camera); verts = new[] { new VertexPositionColor(new Vector3(-1, 1, 0), Color.Blue), new VertexPositionColor(new Vector3(1 , 1, 0), Color.Yellow), new VertexPositionColor(new Vector3(-1,-1, 0), Color.Green), new VertexPositionColor(new Vector3( 1,-1, 0), Color.Red), }; // vertex vertexBuffer = new VertexBuffer(GraphicsDevice, typeof(VertexPositionColor), verts.Length, BufferUsage.None); vertexBuffer.SetData(verts); // effect effect = new BasicEffect(GraphicsDevice); } protected override void UnloadContent() { } protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); KeyboardState keyboardState = Keyboard.GetState(); if (keyboardState.IsKeyDown(Keys.Left)) worldTranslation *= Matrix.CreateTranslation(-.01f, 0, 0); if (keyboardState.IsKeyDown(Keys.Right)) worldTranslation *= Matrix.CreateTranslation(.01f, 0, 0); if (keyboardState.IsKeyDown(Keys.Up)) worldTranslation *= Matrix.CreateTranslation(0, .01f, 0); if (keyboardState.IsKeyDown(Keys.Down)) worldTranslation *= Matrix.CreateTranslation(0, -.01f, 0); if (keyboardState.IsKeyDown(Keys.I)) worldScale *= Matrix.CreateScale(1.02f); if (keyboardState.IsKeyDown(Keys.O)) worldScale *= Matrix.CreateScale(0.98f); worldRotation *= Matrix.CreateRotationY(MathHelper.PiOver4 / 60); base.Update(gameTime); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); GraphicsDevice.SetVertexBuffer(vertexBuffer); // effect.World = worldScale * worldRotation * worldTranslation; effect.View = camera.View; effect.Projection = camera.Projection; effect.VertexColorEnabled = true; foreach (var pass in effect.CurrentTechnique.Passes) { pass.Apply(); GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleStrip, verts, 0, 2); } base.Draw(gameTime); } }
Compilando e executando esse programa, temos algo assim:
Repare que tudo que fizemos foi rever a lista de pontos e informar DrawUserPrimitives para desenhar dois triângulos.
Mas espere! Dois triângulos a partir de quatro pontos?! Como? Simples, instruímos DrawUserPrimitives a usar a PrimitiveType.TriangleStrip. Essa PrimitiveType orienta XNA a desenhar um primeiro triângulo usando os três primeiros pontos do buffer, o segundo, usando o quarto ponto, e os dois últimos do triângulo anterior…. e assim sucessivamente.
Há outros tipos de primitivas, mas discitiremos em um próximo post.
Aplicando uma textura
Aplicar texturas? Como assim?! Bem, uma textura é um bitmap 2D. Texturas são desenhadas em em objetos gráficos conforme um mapeamento de suas coordenadas 2D para as coordenadas de uma superfície. Este processo é conhecido, em computação gráfica, como aplicação de textura.
Como fazer isso? Bem, começamos desistindo de usar vértices com coordenadas e cores. No lugar disso, usamos coordenadas 3D e coordenadas UV.
Coordenadas UV correspondem a posições em uma textura. (0,0) corresponde ao canto superior esquerdo da textura, (1,1) corresponde ao canto inferior direito.
Uma textura, para ser aplicada, precisa ser carregada na Content Pipeline. Para essa demo, acrescentei a seguinte figura:
Eis a versão do Game alterada para aplicar a textura:
public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; Camera camera; Texture2D texture; Matrix worldTranslation = Matrix.Identity; Matrix worldRotation = Matrix.Identity; Matrix worldScale = Matrix.Identity; VertexPositionTexture[] verts; VertexBuffer vertexBuffer; BasicEffect effect; public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; } protected override void Initialize() { base.Initialize(); } protected override void LoadContent() { // Create a new SpriteBatch, which can be used to draw textures. spriteBatch = new SpriteBatch(GraphicsDevice); texture = Content.Load< Texture2D >(@"Textures\Lighthouse"); RasterizerState rs = new RasterizerState(); rs.CullMode = CullMode.None; GraphicsDevice.RasterizerState = rs; // camera camera = new Camera(this, new Vector3(0, 0, 5)); Components.Add(camera); verts = new[] { new VertexPositionTexture(new Vector3(-1, 1, 0), new Vector2(0,0)), new VertexPositionTexture(new Vector3(1 , 1, 0), new Vector2(1,0)), new VertexPositionTexture(new Vector3(-1,-1, 0), new Vector2(0,1)), new VertexPositionTexture(new Vector3( 1,-1, 0), new Vector2(1,1)) }; vertexBuffer = new VertexBuffer(GraphicsDevice, typeof(VertexPositionTexture), verts.Length, BufferUsage.None); vertexBuffer.SetData(verts); // effect effect = new BasicEffect(GraphicsDevice); } protected override void UnloadContent() { } protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); KeyboardState keyboardState = Keyboard.GetState(); if (keyboardState.IsKeyDown(Keys.Left)) worldTranslation *= Matrix.CreateTranslation(-.01f, 0, 0); if (keyboardState.IsKeyDown(Keys.Right)) worldTranslation *= Matrix.CreateTranslation(.01f, 0, 0); if (keyboardState.IsKeyDown(Keys.Up)) worldTranslation *= Matrix.CreateTranslation(0, .01f, 0); if (keyboardState.IsKeyDown(Keys.Down)) worldTranslation *= Matrix.CreateTranslation(0, -.01f, 0); if (keyboardState.IsKeyDown(Keys.I)) worldScale *= Matrix.CreateScale(1.02f); if (keyboardState.IsKeyDown(Keys.O)) worldScale *= Matrix.CreateScale(0.98f); worldRotation *= Matrix.CreateRotationY(MathHelper.PiOver4 / 60); base.Update(gameTime); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); GraphicsDevice.SetVertexBuffer(vertexBuffer); effect.World = worldScale * worldRotation * worldTranslation; effect.View = camera.View; effect.Projection = camera.Projection; effect.Texture = texture; effect.TextureEnabled = true; foreach (var pass in effect.CurrentTechnique.Passes) { pass.Apply(); GraphicsDevice.DrawUserPrimitives (PrimitiveType.TriangleStrip, verts, 0, 2); } base.Draw(gameTime); } }
Compilando e executando, temos o seguinte resultado:
Bom, acho que chega!
Por hoje, era isso.
Posted on março 19, 2011 by elemarjr
0