Introdução ao DirectX

Microsoft Corporation

Introdução

Bem-vindo ao segundo artigo sobre como iniciar o desenvolvimento de um jogo. Neste artigo, vamos abordar os fundamentos do DirectX.

O DirectX é uma API multimídia que oferece uma interface padrão para interagir com elementos gráficos, placas de som e dispositivos de entrada, entre outros. Sem este conjunto padrão de APIs, você precisaria escrever um código diferente para cada combinação de elementos gráficos e placas de som, e para cada tipo de teclado, mouse e joystick. O DirectX nos distancia do hardware específico e traduz um conjunto comum de instruções em comandos específicos de hardware.

Como todas as ferramentas novas, o DirectX tem vários termos e definições novos que você precisa entender. Além desses novos termos, também será necessário recordar suas habilidades matemáticas. O DirectX e o desenvolvimento de jogos em geral utilizam muita matemática, e isso será útil se você compreender os fundamentos. Não é necessário desenterrar a velha calculadora; a idéia é entender o objetivo e a maneira de chegar a esse objetivo. O restante é feito em código, com várias bibliotecas matemáticas predefinidas.

Visão geral do DirectX

O DirectX apareceu pela primeira vez em 1995 e foi então chamado de "GameSDK". Em seu formato original, ele foi direcionado aos desenvolvedores que usavam C e C++. Apenas com o lançamento da primeira versão gerenciada (9.0) da API em dezembro de 2002, foi possível usar o C# ou o VB.NET com o DirectX (na verdade, você pode usar qualquer linguagem compatível com CLR, se desejar).

Muito foi dito e escrito sobre o desempenho do DirectX gerenciado em comparação com a versão não gerenciada, mas o fato de os jogos comerciais já terem sido criados usando o DirectX gerenciado deve acabar com essa discussão de uma vez por todas. Embora determinados jogos com necessidades de desempenho extremas possam precisar usar código não gerenciado, a maioria dos jogos pode ser criada com código gerenciado ou com uma combinação de código gerenciado e não gerenciado. Escrever em código gerenciado torna os desenvolvedores mais produtivos, permitindo que eles criem código em maior quantidade e com mais segurança.

Depois de instalar o SDK do DirectX, você deverá ter um diretório em C:\WINDOWS\Microsoft.NET\Managed DirectX com um subdiretório para cada versão do SDK que foi instalada em sua máquina. Eu estou na quarta versão na máquina que estou usando e, conseqüentemente, tenho quatro subdiretórios. Em cada subdiretório, deve haver nove DLLs e nove arquivos XML. Como o mundo gerenciado do .NET nos permite ter várias versões do mesmo arquivo DLL no mesmo computador sem causar os problemas que antes formavam o chamado inferno das DLLs, podemos ter várias versões das bibliotecas do DirectX gerenciado a nosso dispor. Isso permite que você faça facilmente a reversão para uma versão anterior depois de instalar uma nova versão.

Se você tiver alguma experiência anterior com arquivos DLL no Windows, poderá estar preocupado com o fato de que ter várias versões do mesmo arquivo instaladas no mesmo computador poderá causar problemas. Essas questões de versão não são mais um problema desde a introdução das versões lado a lado no .NET. Isso significa que, quando uma nova versão do SDK é lançada, você pode usar as diversas versões para verificar questões de compatibilidade, sem precisar se comprometer com uma atualização.

Os nove arquivos DLL correspondem, em termos gerais, aos dez espaços para nome no DirectX. À medida que criarmos nosso jogo, usaremos vários desses espaços para nome para oferecer suporte a dispositivos de entrada, som, jogos em rede e, naturalmente, elementos gráficos 3D.

Espaço para nome

Descrição

Microsoft.DirectX

Estruturas matemáticas e classes comuns

Microsoft.DirectX.Direct3D

Elementos gráficos 3D e bibliotecas auxiliares

Microsoft.DirectX.DirectDraw

API gráfica do Direct Draw. Esse é um espaço para nome herdado e provavelmente você não precisará usá-lo.

Microsoft.DirectX.DirectPlay

API de rede para jogos com vários participantes

Microsoft.DirectX.DirectSound

Suporte para som

Microsoft.DirectX.DirectInput

Suporte para dispositivos de entrada (ou seja, mouse e joystick)

Microsoft.DirectX.AudioVideoPlayback

Execução de vídeo e áudio (ou seja, reprodução de DVDs no PC)

Microsoft.DirectX.Diagnostics

Solução de problemas

Microsoft.DirectX.Security

Segurança de acesso

Microsoft.DirectX.Security.Permissions

Permissões de segurança de acesso

Antes de continuarmos, precisamos concluir alguns pontos que não fechamos no último artigo. Depois de adicionar a classe FrameworkTimer, não pudemos mais compilar o projeto porque estavam faltando as referências ao DirectX. Vamos corrigir isso agora e adicioná-las.

  • Clique com o botão direito do mouse em References (Referências) no Solution Explorer e selecione Add Reference (Adicionar Referência).

  • Na guia .NET, role para baixo até localizar o componente chamado Microsoft.DirectX

  • Mantendo a tecla CTRL pressionada, selecione os componentes Microsoft.DirectX, Microsoft.DirectX.Direct3D e clique em OK.

  • A última etapa que precisamos concluir antes de poder finalmente compilar a solução é inserir comentários nas partes do arquivo dxmutmisc.cs que não são necessárias.

  • Abra o arquivo dxmutmisc.cs e insira comentários em todo o código, com exceção das regiões Native Methods e Timer.

  • Compile a solução (pressione F6). Se você fez tudo certo, a solução será compilada agora.

A minha GPU é maior que a sua

Antes de nos aprofundar na API do DirectX, vamos voltar um pouco e pensar sobre o que estamos tentando fazer. Para criar um jogo rápido, precisamos usar algum tipo de processador que nos permita calcular a imagem real que será mostrada no monitor. Como nenhum de nós tem monitores 3D, essa imagem será 2D. Então, precisamos de um pouco de matemática para calcular todos os quadros convertendo modelos 3D em imagens 2D. Se usássemos a CPU do computador para todos esses cálculos, nosso jogo seria executado mais lentamente, pois também temos que usar a mesma CPU para processar a IA, verificar a entrada e, obviamente, executar o sistema operacional e todos os processos em segundo plano. Se pudermos passar o cálculo da parte dos elementos gráficos para um processador separado, poderemos acelerar as coisas.

As placas gráficas modernas têm seu próprio processador, chamado unidade de processamento gráfico ou GPU (Graphics Processing Unit). Essas GPUs são processadores especializados otimizados para fazer o tipo de cálculos de que precisamos. Além disso, cada placa gráfica também tem sua própria memória que a torna, na prática, um computador separado dentro do nosso computador. Isso significa que, independentemente do tamanho e da rapidez do seu computador básico, a velocidade gráfica depende mais da GPU e da memória de vídeo do que de qualquer outra coisa.

Adaptadores e dispositivos

A maioria das placas gráficas permite que apenas um monitor seja conectado de cada vez, mas algumas oferecem suporte a vários monitores. Você também poderia ter mais de uma placa gráfica em seu computador. Independentemente da sua configuração, cada placa gráfica tem um Adaptador. Você pode pensar nele como a placa de vídeo física do computador. Os adaptadores têm "nomes" sob o ponto de vista computacional; o primeiro adaptador, ou o padrão, é "nomeado" 0, o segundo adaptador, 1 e assim por diante. No DirectX, você não interage diretamente com o adaptador. Em vez disso, você se conecta a um adaptador usando um Dispositivo.

Um dispositivo representa uma conexão com um adaptador específico; cada adaptador pode ter vários dispositivos associados a ele. O DirectX dá suporte a três tipos de dispositivos: Hardware, Referências e Software. Nós usaremos o tipo Hardware para o nosso jogo, pois ele fornece a velocidade de que precisamos para executar o jogo.

Agora, é hora de criar o dispositivo que vamos usar para o nosso jogo. Adicione o seguinte código ao construtor do formulário GameEngine, depois do código existente. Estamos usando o construtor porque podemos garantir que o código contido nele será executado antes de qualquer outra coisa. Isso garante que tenhamos sempre um objeto de dispositivo válido ao qual faremos referência mais tarde.

  • Adicione o seguinte código ao construtor do GameEngine imediatamente após a instrução this.SetStyle:
// Obter o ordinal do adaptador padrão

int adapterOrdinal = Manager.Adapters.Default.Adapter;

 

// Obter os recursos de nosso dispositivo de forma que possamos verificá-los para configurar o CreateFlags

Caps caps = Manager.GetDeviceCaps(adapterOrdinal, DeviceType.Hardware);

CreateFlags createFlags;

// Verificar se a placa gráfica é capaz de

// executar as operações de processamento de vértice

// A opção HardwareVertexProcessing é a melhor

if (caps.DeviceCaps.SupportsHardwareTransformAndLight)

{

createFlags = CreateFlags.HardwareVertexProcessing;

}

else

{

createFlags = CreateFlags.SoftwareVertexProcessing;

}

// Se a placa gráfica oferecer suporte ao processamento de vértice, verifique se o dispositivo pode

// fazer a rasterização, transformações de matrizes e operações de iluminação e sombreamento

// Esta combinação fornece a experiência de jogo mais veloz

if (caps.DeviceCaps.SupportsPureDevice && createFlags == CreateFlags.HardwareVertexProcessing)

{

createFlags |= CreateFlags.PureDevice;

}

// Configurar o PresentParameters, que determina como o dispositivo se comporta

PresentParameters presentParams = new PresentParameters();

presentParams.SwapEffect = SwapEffect.Discard;

// Verificar se estamos no modo de janela ao depurar

#if DEBUG

presentParams.Windowed = true;

#endif

// Agora, criar o dispositivo

device = new Device(

                                adapterOrdinal,

                                DeviceType.Hardware,

                                this,

                                createFlags,

                                presentParams

                                );
  • No final do formulário, logo depois da declaração da variável deltaTime, adicione o seguinte código:
private Device device;

É bastante código apenas para configurar um Dispositivo, mas esta abordagem à configuração do dispositivo é a maneira mais segura de garantir a maximização do desempenho do nosso jogo com base no hardware da placa gráfica. O modo mais fácil de entender esse bloco de código é quebrá-lo em quatro partes distintas.

  1. A primeira linha do código simplesmente obtém o nome do adaptador padrão, que geralmente é 0. Em vez de apostar que é zero, é mais seguro usar a classe Manager para obter o nome do adaptador padrão. Dessa maneira, não dará tudo errado se, por algum motivo, o nome do adaptador padrão for 2, por exemplo.

  2. A seção de código seguinte é usada para determinar as configurações da enumeração CreateFlags que passamos para o construtor do Dispositivo e que rege o comportamento do dispositivo após a criação. Novamente, usamos o Manager para obter uma listagem dos recursos (chamados, de forma abreviada, de Caps) para o adaptador padrão. Então, usamos essa listagem de recursos para determinar se executamos o processamento de vértice no hardware (o que é mais rápido) ou no software (o que é mais lento, mas tem a garantia de sempre funcionar). Na verdade, essa nomenclatura é uma designação incorreta, pois SoftwareVertexProcessing significa que usamos a CPU, enquanto HardwareVertexProcessing usa a GPU. Então, executamos outra verificação para ver se nosso adaptador pode dar suporte a um dispositivo puro, o que significa que a placa gráfica pode fazer a rasterização, as transformações de matrizes e os cálculos de iluminação e sombreamento. Se o dispositivo puder e se a verificação anterior tiver determinado que podemos usar o processamento de vértice no hardware, adicionaremos a configuração PureDevice à enumeração CreateFlags. A combinação de HardwareVertexProcessing e PureDevice nos fornece o melhor desempenho possível. Sendo assim, queremos usar essa combinação, se possível.

  3. O parâmetro final necessário para criar o Dispositivo é o objeto PresentParameters. Esse objeto determina como o dispositivo apresenta seus dados na tela, daí o nome. Primeiro, definimos a enumeração SwapEffect, que determina como o buffer e o dispositivo se relacionam. Ao selecionar a opção Discard, estamos optando por simplesmente descartar o buffer de fundo e gravar diretamente no buffer frontal. Dentro da instrução If, determinamos se o aplicativo está sendo executado no modo de depuração. Se estivermos no modo de depuração, não desejaremos executar no modo de tela inteira, que é o padrão, pois ele torna a depuração muito difícil. Usar esse método para determinar a configuração é melhor do que embuti-la no código e, depois, esquecer de alterá-la ao lançar o jogo.

  4. A etapa final é realmente criar o dispositivo. Passamos o ordinal do adaptador padrão, a janela à qual desejamos ligar o dispositivo, o tipo de dispositivo e, em seguida, passamos os objetos CreateFlags e PresentParameters que criamos anteriormente.

O efeito final desse código todo é que temos um dispositivo válido que podemos usar para desenhar a tela. Para usar o dispositivo, precisamos adicionar duas linhas de código dentro do loop de processamento.

  • Adicione o seguinte código ao método OnPaint imediatamente após a instrução FrameworkTimer.Start().
device.Clear(ClearFlags.Target, Color.DarkBlue, 1.0f, 0);

device.Present();

A primeira linha limpa a janela com a cor indicada no segundo parâmetro (você pode usar qualquer cor de janela predefinida na enumeração Color). Os dois últimos parâmetros do método Clear descrevem os valores da profundidade z e do estêncil, e não são importantes neste momento.

O método Present do dispositivo o faz exibir o conteúdo do buffer de fundo na tela. A tela também é chamada de buffer frontal e a interação entre esses buffers é determinada pela enumeração SwapEffect que você definiu anteriormente.

Agora, executamos a solução e pronto: temos uma tela azul. Embora isso não seja muito impressionante e pareça muito código para se obter uma simples tela azul, nós acabamos de integrar o DirectX ao nosso GameEngine com êxito.

Terminologia dos elementos gráficos 3D

Antes de continuarmos e processarmos o terreno e as unidades, precisamos voltar um minuto e abordar alguns dos princípios e definições usados na programação de elementos gráficos tridimensionais. Não estamos fazendo isso para nos exibir para nossos amigos, mas porque esses princípios e termos fornecem a base para a programação com elementos gráficos tridimensionais.

O jogo que estou jogando no momento se chama Brothers in Arms: Road to Hill 30 (www.brothersinarmsgame.com). É um jogo em que você é o atirador e que se passa na Normandia, França, em 1942. Todo o terreno, os prédios, as estradas, os rios etc. no jogo são réplicas exatas do terreno original da Normandia em 1942. Seus criadores usaram fotografias aéreas e mapas para recriar o terreno e viajaram para a França para pesquisá-lo pessoalmente. Eles usaram essas informações para recriar o terreno original para o jogo. Quando precisamos descrever a localização de um ponto em algum lugar do mundo, como o Space Needle em Seattle, freqüentemente usamos um sistema de coordenadas chamado sistema de coordenadas geográficas (http://en.wikipedia.org/wiki/Geographic_coordinate_system, em inglês). Nesse sistema, expressamos cada ponto como um par Latitude/Longitude exclusivo (o Space Needle está localizado em: Lat.: 47,62117, Long.: -122,34923).

Quando os criadores do jogo transferiram o terreno para o computador, eles não puderam simplesmente fornecer as coordenadas de Latitude/Longitude para o DirectX, porque o computador não tinha idéia de como representar coordenadas geográficas. Para poder colocar objetos em um mundo tridimensional, precisamos criar um sistema de coordenadas que o computador entenda e transformar as coordenadas de um sistema em outro. O sistema de coordenadas usado no DirectX é o sistema de coordenadas cartesianas da mão esquerda.

Sistema de coordenadas cartesianas

Para posicionar corretamente os objetos em um mundo tridimensional, precisamos saber onde colocá-los e como definir cada ponto claramente. Nós conseguimos isso usando o sistema de coordenadas cartesianas. Esse sistema de coordenadas cartesianas compreende três eixos com ângulos retos entre si, sendo o ponto de intersecção entre eles chamado origem (provavelmente você está mais familiarizado com a versão bidimensional, com os eixos x e y apenas, usado na geometria do ensino médio). Duas propriedades adicionais são necessárias para definir totalmente esse sistema de coordenadas tridimensional: a direção e a orientação.

Direção

Para adicionar uma pitada de desafio, há duas maneiras de representar um sistema de coordenadas cartesianas tridimensional: pela mão esquerda e pela mão direita. Se você seguir o link http://en.wikipedia.org/wiki/Cartesian_coordinate_system (em inglês), poderá descobrir mais informações sobre as razões por trás desse sistema, mas o principal ponto a ser lembrado é que o DirectX usa o sistema de coordenadas da mão esquerda. O resultado final de usar o sistema de coordenadas da mão esquerda é que quanto maior o valor de Z, maior a distância, enquanto no sistema da mão direita os valores ficam menores conforme a distância aumenta. Você precisa saber a direção do espaço de coordenadas para comparar dois objetos usando seus valores de Z e para saber qual está mais distante de você.

Orientação

O único ponto adicional a observar sobre o sistema de coordenadas cartesianas tridimensional é que ele pode ter orientações diferentes dependendo de como o eixo z é desenhado. Se ele for desenhado verticalmente como mostrado abaixo, no gráfico à esquerda, ele será chamado de orientação de coordenadas globais; se for desenhado como à direita, será chamado de orientação de coordenadas locais.

Independentemente da orientação usada para fazer referência aos seus pontos, as coordenadas devem ser transformadas em coordenadas de tela (espaço bidimensional de tela) para que possam ser desenhadas na tela.

Cc518041.DirectX_01(pt-br,MSDN.10).jpg

Vetor

Cada ponto no sistema de coordenadas tridimensional é definido por três valores: os valores X, Y e Z. Para facilitar as coisas, o DirectX nos fornece uma estrutura chamada Vector3 que nos permite armazenar essas coordenadas. Contudo, isso é apenas para nossa conveniência, pois um vetor é definido como um objeto que indica a direção e a velocidade, e não uma localização. Os métodos da classe de vetores são usados para esse fim; mas, por enquanto, essa é uma estrutura conveniente para armazenar os três valores. Dependendo da sua necessidade, também é possível usar as estruturas Vector2 ou Vector4.

Vértice

No código que adicionamos para conectar ao dispositivo, você deve ter notado que precisamos determinar se devemos fazer o processamento de vértice no software ou no hardware. Então, o que é o processamento de vértice e o que é um vértice? Um vértice é um ponto que faz parte de um objeto tridimensional. Um vértice consiste em um vetor e informações adicionais relativas ao mapeamento da textura.

Textura

Uma textura é simplesmente um bitmap 2D que é aplicado a um objeto 3D para dar a ele algum tipo de aparência (textura), como a de grama, concreto, etc.

Malha

Neste artigo, não vamos abordar a utilização de malhas; a definição de malha está ligada à do vértice, então vamos deixá-la de lado por enquanto. Uma malha é formada pelos dados que descrevem uma forma tridimensional. Esses dados incluem a lista de vértices da forma, bem como as informações que descrevem como os vértices são conectados e as informações relativas às texturas que os cobrem.

Como você pode ver, uma malha é formada de vértices, que por sua vez contêm um vetor. Cada nível simplesmente adiciona algumas informações relacionadas a como as partes separadas estão relacionadas entre si. De agora em diante, quando você ouvir falar em vetor, pense em ponto; quando ouvir falar de vértice, pense em ponto e dados e, quando ouvir falar em malha, pense em vários pontos e mais dados.

Triângulos

Agora que você sabe que um vértice é, na verdade, apenas um ponto no espaço 3D, precisamos abordar como os pontos se combinam para formar os objetos. Cada objeto no DirectX é composto de um ou mais triângulos. Isso pode parecer estranho no início, mas é realmente possível representar qualquer forma 2D ou 3D usando triângulos. O motivo para isso é simples: o triângulo é o polígono coplanar mais simples (todos os pontos do triângulo estão no mesmo plano). Isso simplifica a matemática necessária para executar todos os cálculos.

Tudo isso pode parecer muito para aprender, mas é importante entender essas idéias básicas antes de entrar nas matrizes. No próximo artigo, vamos falar sobre as matrizes, sobre o princípio por trás de uma câmera (do ponto de vista do DirectX) e sobre as transformações. Também vamos explicar o que são as malhas e como as informações de mapeamento da textura de um vértice são usadas.

Um contador da taxa de quadros

Como mencionei no primeiro artigo, adicionaremos um contador da taxa de quadros ao nosso jogo. É importante saber a taxa de quadros do jogo, pois ela determina sua velocidade. Além disso, saber agora o que é a taxa de quadros, antes de adicionar qualquer código ao loop de processamento, nos mostrará como cada adição afeta a velocidade do jogo.

  • Adicione uma nova classe ao projeto e denomine-a FrameRate.

  • Adicione o seguinte código dentro da declaração de classe.

public static int CalculateFrameRate()

{

if (System.Environment.TickCount - lastTick >= 1000)

{

lastFrameRate = frameRate;

frameRate = 0;

lastTick = System.Environment.TickCount;

}

 

frameRate++;

 

return lastFrameRate;

}

private static int lastTick;

private static int lastFrameRate;

private static int frameRate;
  • No método OnPaint do GameEngine, adicione a seguinte linha de código imediatamente após a atribuição de deltaTime.
this.Text = string.Format("The framerate is {0}", FrameRate.CalculateFrameRate());

Esse contador da taxa de quadros usa a propriedade TickCount da classe System, que é um invólucro do método GetTickCount da API WIN32 e tem uma precisão de aproximadamente 15 milissegundos. Embora a classe FrameworkTimer seja mais precisa, essa precisão é suficiente para calcular a taxa de quadros.

Manutenção rotineira do código

Antes de terminarmos, há duas alterações que fiz no código original. Primeiro, removi a instrução Application.EnablRTLMirroring() da classe Program. Esse método foi descartado na versão Beta2 do .NET Framework. A outra alteração que fiz foi envolver a classe GameEngine em uma instrução Using. Isso garante que, independentemente do que acontecer à classe GameEngine, ela sempre estará disposta corretamente quando fecharmos o aplicativo.

Resumo

Neste ponto, você deve estar pensando se em algum momento vamos começar a trabalhar no nosso jogo. Um dos desafios de aprender o desenvolvimento de jogos é que você precisa criar uma boa base no início, de forma que as idéias mais avançadas façam sentido depois. Depois de compreender os termos e princípios dos elementos gráficos 3D, você está livre para se concentrar na criação do jogo.

Neste artigo, apresentamos o DirectX, que é a API que usaremos para os elementos gráficos 3D, o controle de dispositivos de entrada e o som no BattleTank 2005. Então, discutimos o que é uma GPU e porque ela é tão importante nos jogos atuais. Também abordamos o que são um Adaptador e um Dispositivo, e como configurá-los no DirectX. Em seguida, entramos no mundo dos termos e das definições que precisamos entender para ir adiante. Finalmente, adicionamos um contador da taxa de quadros ao jogo para acompanhar o desempenho do jogo.

No próximo artigo, abordaremos matrizes e transformações, como colocar uma câmera no mundo 3D e o que são os recursos de recorte e eliminação. Nesse artigo, também adicionaremos a paisagem ao nosso jogo.

Espero que seu primeiro mergulho no mundo do DirectX não o tenha desencorajado. Como algumas definições e alguns princípios são difíceis de entender apenas através da leitura, minha sugestão é que você escreva um pouco de código e experimente as várias configurações para ver o que acontece. O SDK do DirectX também inclui muitos exemplos e tutoriais que abordam esse mesmo assunto e permitem que você experimente as configurações.