Aqui inicio uma série de artigos voltados para renderização 3D em XNA. Começando com os básicos de gráfico 3D, conversarei sobre geração de malha procedural, assim como renderização de modelos exportados de programas de modelagem, texturização por programação e shaders.

Decidi utilizar XNA como base para estes artigos pela sua facilidade de utilização, além de permitir uma fácil adaptação para C++ com DirectX.

Neste artigo, estarei explicando o básico de renderização 3D, gerando triângulos por programação e renderizando-os com cores básicas e com texturas.

A pedidos de alguns leitores, estou editando o artigo para colocar aqui uma pequena explicação sobre o que é XNA, e onde pode baixar.

O XNA é uma framework dotNET da Microsoft. É uma biblioteca de classes e ferramentas para desenvolver facilmente jogos para Windows e XBox 360 utilizando a linguagem C#, mas mantendo uma estrutura semelhante ao DirectX (tanto que sempre aconselho começar com XNA para quem está querendo começar a estudar DirectX). Você pode fazer download do Visual C# 2008 Express Edition e do XNA Game Studio 3.1 AQUI. Ambas as ferramentas são grátis.

Antes de começar o artigo, é importante salientar que o XNA necessita de shaders para renderizar qualquer coisa na tela. Como iremos aprender sobre shaders em outros artigos mais adiante, cada artigo anterior a esse estudo irei preparar um arquivo de shader específico para podermos trabalhar tranquilamente cada assunto tratato. O arquivo de shader para este artigo pode ser BAIXADO AQUI.

Comece por criar um projeto XNA Windows Game 3.1.

Para utilizar o arquivo de shader, é preciso fazer o seguinte. Primeiro, copie o arquivo de shader .fx para a pasta Content, dentro da pasta do seu projeto XNA. Em seguida, importe-o para o projeto dentro do Visual C#, clicando com o butão direito na pasta Content, e escolhendo Import Existing Item. Agora, temos algumas linhas de código necessárias para utilizar esse shader. No arquivo de código Game1.cs, logo em cima ao código criando o GraphicsDeviceManager e o SpriteBatch, coloque o seguinte código:

Effect effect;

Dentro do método LoadContent(), coloque:

effect = Content.Load<Effect>("article01");


E finalmente, precisamos criar a base de renderização com esse shader, com o seguinte código dentro do método Draw():

// limpar a tela com a cor preta
GraphicsDevice.Clear(Color.Black);
 
// setando o efeito padrão - explicarei melhor esta parte mais tarde
effect.CurrentTechnique = effect.Techniques[0];
// aqui começa a renderização utilizando este shader
effect.Begin();
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
   // cada shader pode ter vários passos - explicarei mais tarde também
   pass.Begin();
 
   // AQUI IREMOS COLOCAR NOSSO CÓDIGO DE RENDERIZAÇÃO DE MALHA
 
   // termina o passo
   pass.End();
}
// termina renderização com este shader
effect.End();

Pronto, temos agora a base necessária para renderizar o nosso exercício de hoje.

Qualquer objeto renderizado em 3D é representado por um conjunto de triângulos. Cada triângulo é (logicamente) definido por 3 pontos, que chamaremos de vértices. Cada vértice, por sua vez, tem um conjunto de características definidas, como por exemplo posição espacial ou cor.

No XNA, existem algumas estruturas prontas com definições de vértice, então iremos utilizar essa facilidade para começar. A estrutura VertexPositionColor define um vértice com posição e cor. Para definir um triângulo, necessitamos de 3 vértices, então vamos criar um array e um método para inicializar nosso array:

VertexPositionColor[] vertices;
private void CriaVertices()
{
   vertices = new VertexPositionColor[3];
 
   vertices[0].Position = new Vector3(-0.5f, -0.5f, 0);
   vertices[0].Color = Color.Green;
   vertices[1].Position = new Vector3(0, 0.5f, 0);
   vertices[1].Color = Color.Yellow;
   vertices[2].Position = new Vector3(0.5f, -0.5f, 0);
   vertices[2].Color = Color.Red;
}

Repare que eu estou colocando as coordenadas diretamente no espaço 2D da tela, assim não existe necessidade de transformar de 3D para 2D (iremos ver isso mais tarde). Agora que temos nosso triângulo definido, precisamos ainda definir o formato dos nossos vértices, para que a placa de vídeo saiba utilizar os dados do nosso array. Para isso, utilizamos a estrutura VertexDeclaration, da seguinte forma:

VertexDeclaration vertexDecl;
private void CriaDecl()
{
   vertexDecl = new VertexDeclaration(GraphicsDevice, VertexPositionColor.VertexElements);
}

Agora, basta chamar estes dois métodos para inicializar nossos dados e estamos prontos para renderizar. Coloque a chamada para ambos os métodos criados dentro do método LoadContent(), logo após a leitura do Effect. Basta colocarmos o código que vai enviar os dados para a a placa de vídeo e renderizá-los, dentro do loop Begin()..End() dos passos do nosso shader, no método Draw()

// aqui setamos a declaração do formato de vertices no directx
GraphicsDevice.VertexDeclaration = vertexDecl;
// aqui mandamos o directX desenhar nosso triângulo
GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, vertices, 0, 1);

Rode a aplicação e você deverá ver um triângulo desenhado na tela, onde cada ponta tem uma cor diferente. Vamos entender um pouco mais sobre este último pedaço de código.

A primeira linha passa a declaração do formato dos vértices. O array de vértices, ao ser passado para a GPU, é transformado num stream de bytes, e é esta declaração que vai definir o tamanho nesse stream de cada vértice, assim o que cada dado significa, e sem isto a GPU não saberia o que fazer com os dados.
A segunda linha manda desenhar nosso triângulo, e recebe 4 parametros:

  • O primeiro parâmetro, PrimitiveType.TriangleList, define o processo seguido para renderizar os triângulos. Neste caso, TriangleList, significa que a GPU irá utilizar o stream como uma lista de vértices, desenhando triângulos utilizando 3 vértices por vez. Outras opções utilizam a stream de jeitos diferentes, e iremos estudar essas técnicas mais tarde;
  • O segundo parâmetro é o nosso array de vértices, que passamos aqui para o DirectX mandar para a placa de vídeo;
  • O terceiro parâmetro é o vertexOffset. Basicamente, qual o índice do array que a GPU deve usar como primeiro vértice. No nosso caso, começamos no início do buffer, ou seja, índice 0;
  • Finalmente, o ultimo parâmetro define o número de triângulos a serem desenhados. Se quisermos aumentar este número, é necessário a criação de mais vértices no nosso array.

Ok. Chegamos ao fim do nosso primeiro artigo sobre renderização 3D. No próximo artigo, iremos estudar como utilizar coordenadas 3D de mundo (para não ter que definir coordenadas 2D de tela diretamente nos vértices), e como transformar essas coordenadas 3D em 2D na tela.