.NET

20 jul, 2007

Games: Entendendo a classe PresentParameters

Publicidade

IDE: Microsoft Visual Studio 2005 (.NET Framework 2.0, DirectX SDK [April 2007])

Olá amigos! Mês passado apresentei pra vocês meu primeiro artigo sobre desenvolvimento de jogos utilizando o framework do DirectX, abordamos todos os tópicos básicos e teóricos da metodologia 3D. Aprendemos também, na prática, como criar nosso dispositivo, um handle para a placa de vídeo, e limpamos nossa tela com uma cor especificada.

Hoje começo meu segundo artigo explicando a classe PresentParameters que contém diversas propriedades que definem os parâmetros de apresentação do nosso Device window. Como estou buscando mais detalhes, procurando ir o mais fundo possível no assunto, decidi mergulhar nesta classe e conhecer suas diversas propriedades para que possamos pegar todos os detalhes possíveis desta fase inicial de criação. É muito importante entender os aspectos básicos do sistema, seu funcionamento e lógica encapsulada. Por isso quanto mais você conhecer da biblioteca, mais chances terá de ter sucesso com a mesma.

Logo em seguida introduziremos um exemplo prático onde setamos diversas propriedades da classe, inicializando nosso aplicativo pela primeira vez em tela cheia. Confira a declaração da classe:

public sealed class PresentParameters : ICloneable

PresentParameters é uma classe pública que define os parâmetros de apresentação de nosso dispositivo. Ela não pode ser herdada pois esta definida como sealed. Ela implementa a interface ICloneable que contém o método Clone, utilizada para clonagens do tipo shallow copy e deep copy.

Microsoft.DirectX.Direct3D.PresentParameters.

  • Windowed: Se true, a aplicação será executada em modo de janela. Se false, em tela cheia.
  • SwapEffect: Como descrito no artigo anterior, selecionaremos Discard pois, deixamos assim que o compilador escolha o melhor método de swap chain a ser utilizado. É importante lembrar que Discard é o único swap effect que pode ser utilizado quando setamos algum valor diferente de None da enumeration MultiSampleType para PresentParameters.MultiSample (um método de anti-aliasing, veremos mais detalhes em artigos futuros). Assim como em Flip, Discard pode incluir múltiplos back buffers, podendo estes serem acessados pelos métodos Device.GetBackBuffer ou SwapChain.GetBackBuffer. Outro detalhe importante, é que como escolhemos esta opção, devemos atualizar todo o conteúdo de nosso back buffer antes de chamarmos o método Present da classe Device. Já para os debuggers, a versão debug de tempo de execução sobrescreve o conteúdo de back buffers descartados com dados randomicos, permitindo que os desenvolvedores verifiquem se a superfície de suas aplicações estão sendo atualizadas corretamente. Não se esqueça que em um swap chain em tela cheia, o presentation rate (vamos chamá-lo assim ao invés de frame rate) pode ser obtido por Caps.PresentationInterval assim que o Device ou swap chain é criado. A menos que seu valor seja Immediate, o rate é sincronizado com o vertical sync do monitor. Em modo janela este ocorre sempre através da cópia imediata. (Para maiores detalhes sobre esta, leia meu artigo Introdução à programação de jogos e aplicativos 3D com DirectX para código gerenciado [07/04/2007]).
  • EnableAutoDepthStencil: Se true, o Direct3D será o gerenciador de depth buffers de nossa aplicação. Note que escolhendo esta opção, você obrigatoriamente terá que setar um valor para o formato do depth buffer definido na classe AutoDepthStencilFormat. Quando o nosso Device é criado, ele automaticamente cria o depth buffer e o seta como alvo de renderização. Quando o mesmo é resetado, ele também é automaticamente destruído é recriado com seu novo tamanho. Este membro é falso por padrão.
  • AutoDepthStencilFormat: Obtém ou ajusta o DepthFormat que configura o depth stencil surface criado pelo dispositivo. Esta surface, é uma área na memória bastante similar ao back buffer mas, ela trabalha com informações diferentes e é ignorada a menos que a propriedade EnableAutoDepthStencil esteja setada como true. A primeira vista, os membros desta enum parecem ser bem estranhos. Mas conhecendo-os melhor, você verá que não passam de simples acrônimos para uma nomeação mais simples. Por exemplo o membro D15S1. Este membro contém um z-buffer de 16 bits sendo que 15 bits são reservados para o canal de profundidade enquanto 1 bit é reservado para o canal stencil. Veja a documentação aqui. Note que no canal de profundidade (D), o driver pode consumir mais bits do que o especificado pelo programador.
  • DeviceWindow: Um Control que representa a janela de saída. Para aplicações em tela cheia, a janela de desenho padrão é aquela que o aplicativo está sendo executado. Para aplicações que utilizam múltiplos Devices de tela cheia (como sistemas que utilizam múltiplos monitores), apenas um Device pode usar a janela em foco enquanto os outros demais, devem conter Devices de janelas únicos. Para aplicações em modo de janela, a janela de desenho padrão é aquela referida pelo método Present.
  • DeviceWindowHandle: Um ponteiro para um objeto que representa uma janela de saída. Os mesmos detalhes de DeviceWindow são válidos aqui.
  • BackBufferWidth: A largura do nosso back buffer em pixels. Note que este valor representa a resolução do jogo. Se nosso aplicativo estiver sendo executado em tela cheia, certifique-se que este valor corresponde aos valores suportados pela classe DisplayModeCollection que manipula uma coleção de structures do tipo DisplayMode. Caso PresentParameters.Windowed seja true, este valor deverá ser 0 que corresponde à largura atual do formulário.
  • BackBufferHeight: O mesmo de BackBufferWidth com a diferença de que definimos um valor para a altura, e não para a largura.
  • BackBufferFormat: Define o formato do back buffer. Caso esteja executando sua aplicação em modo janela, você pode especificar o formato Unknow para que o runtime utilize o display mode atual evitando a chamada DisplayMode para o Device da janela. É muito importante lembrar que em full screen mode, é impossível fazer conversão de cores.
  • BackBufferCount: O número de back buffers. Este valor pode ser 0, 1, 2 ou 3 (não se esqueça que 0 é tratado como 1). O método falha se pelo menos um back buffer não for criado. Não se esqueça que Copy exige a presença de apenas 1 buffer.
  • MultiSample: Define os níveis de multisampling de anti-aliasing na cena atual. Trataremos de anti-aliasing em artigos futuros.
  • MultiSampleQuality: O nível de qualidade do anti-aliasing. Este valor varia de 0 até (1 – [o valor retornado pelo método CheckDeviceMultiSampleType]).
  • FullScreenRefreshRateInHz: Obtém ou ajusta o valor indicando a taxa em que o monitor atualizará a tela. Esta propriedade recebe a taxa de atualização em hertz definida na coleção DisplayModeCollection, ou 0 caso estivermos em modo de janela.
  • PresentationInterval: Define a taxa máxima de presentation rate para um swap chain de back buffers. Em modo janela este valor deve ser igual a PresentInterval.Default (0) que é igual a 1. Você poderá encontrar mais informações sobre a enum PresentInterval em http://msdn2.microsoft.com/en-us/library/ms858180.aspx mas por enquanto não trabalharemos com vertical retraces. Em tela cheia, podemos ajustá-lo como 0, ou definir um valor igual a um dos valores definidos nas flags da enumeração Present. Para saber quais valores suportados pelo dispositivo, utilize Caps.PresentationInterval.
  • ForceNoMultiThreadedFlag: Um bool que indica quando a aplicação deverá ou não usar mais de uma thread.
  • PresentFlag: O present flag da aplicação. Esta flag controla a operação Device.Present. Setamos valores para esta propriedade através da enum PresentFlag que por enquanto manteremos como 0, ou None, que discarta qualquer flag de apresentação. Se escolhermos LockableBackBuffer (1), damos a aplicação a habilidade de “trancar” o back buffer diretamente. Chamamos este evento, “trancar”, de lock, garantindo-nos o acesso aos buffers para que possamos por exemplo, aplicar efeitos diversos em cada surface. Aplicações não são lockables por padrão, a menos que você escolha esta opção toda vez que criar um Device ou quando resetá-lo. Note que haverá uma queda no desempenho escolhendo este valor. Escolhendo o membro DiscardDepthStencil (2), deixaremos que o z-buffer descarte os ajustes do Device ou da swap chain quando estes forem criados. Ao setarmos esta flag, os dados do stencil z-buffer são invalidados logo após a chamada dos métodos Device.Present e Device.DepthStencilSurface com uma surface diferente. Descartando os dados do z-buffer podemos aumentar a performace dependendo do driver utilizado. O depurador em tempo de execução nos garantirá a definição de um valor constante para o buffer de profundidade após a chamada dos métodos mencionados acima. Lembre-se que não podemos escolher esta opção para tipos de buffers lockables. O penúltimo membro, DeviceClip (4), obtém um blt de Device.Present para a área do cliente atual. Esta flag só funciona no Windows 2000 e XP. O último membro Video (16) apenas informa ao driver que o back buffer conterá dados de vídeo.

Ufa! Depois de um pouquinho de leitura, conseguimos entender os principais aspectos da classe PresentParameters. Setamos estes valores antes da criação de nosso Device para que ele possa ser inicializado corretamente de acordo com nossas necessidades. Podemos excluir alguns aspectos desnecessários para aumentarmos nosso desempenho, ou incluir diversos que nos trazem dezenas de outras possibilidades.

Agora que temos um overview básico sobre esta classe, vamos criar nossa segunda telinha preta, só que agora em tela cheia (rs). Note que também modificaremos o código da criação do Device, tratando possíveis erros de suporte a vídeo antes da criação do mesmo. Deixarei a explicação para os comentários dentro código. Confira a nova versão do método InicializaGraficos:

public bool InicializaGraficos(bool isWindowed) 
{ 
PresentParameters presentParameters = new PresentParameters(); 
// Decidimos se executaremos em plano janela ou em tela cheia. 
if (isWindowed) 
{ 
presentParameters.Windowed = true; 
presentParameters.SwapEffect = SwapEffect.Discard; 
} 
else 
{ 
presentParameters.SwapEffect = SwapEffect.Discard; 
presentParameters.BackBufferWidth = 800; 
presentParameters.BackBufferHeight = 600; 
presentParameters.BackBufferFormat = Format.A8R8G8B8; 
} 
// Estes parâmetros estão comentados pois são desnecessários no contexto atual de criação do 
// Device e suas inicializações padrão são originais como as ajustadas abaixo. Para os 
// perfeccionistas do desempenho, ocultamos estas configurações pois, elas já são conhecidas 
// inicialmente pelo compilador. 
//presentParameters.EnableAutoDepthStencil = false; 
//presentParameters.AutoDepthStencilFormat = Unknow; 
//presentParameters.Windowed = false; 
//presentParameters.DeviceWindow = this; 
//presentParameters.DeviceWindowHandle = this.Handle; 
//presentParameters.BackBufferCount = 0; // 0 = 1. 
//presentParameters.MultiSample = MultiSampleType.None; 
//presentParameters.MultiSampleQuality = 0; 
//presentParameters.FullScreenRefreshRateInHz = 0; 
//presentParameters.PresentationInterval = PresentInterval.Default; 
//presentParameters.ForceNoMultiThreadedFlag = false; 
//presentParameters.PresentFlag = PresentFlag.None; 
// Pega o número ordinal do Device padrão. 
// Geralmente este número é 0. 
int inteiroAdapter = Manager.Adapters.Default.Adapter; 
//Obtém as capacidades da placa através da struct Caps. 
Caps capacidades = Manager.GetDeviceCaps(inteiroAdapter, DeviceType.Hardware); 
// Verifica as capacidades do dispositivo. 
// Verifica suporte a transformações e luzes utilizadas por HardwareVertexProcessing. 
CreateFlags flagCapacidadeDevice; 
if (capacidades.DeviceCaps.SupportsHardwareTransformAndLight) 
{ 
flagCapacidadeDevice = CreateFlags.HardwareVertexProcessing; 
Console.WriteLine("GameEngine.cs -> HardwareVertexProcessing"); 
} 
else // Senão utilizamos por SoftwareVertexProcessing. 
{ 
flagCapacidadeDevice = CreateFlags.SoftwareVertexProcessing; 
Console.WriteLine("GameEngine.cs -> SoftwareVertexProcessing"); 
} 
// Se a placa de vídeo suportar HardwareVertexProcessing, checa se a mesma suporta rasterização, 
// transformações de matriz, luz e etc... Essa combinação é a mais rápida. 
if (capacidades.DeviceCaps.SupportsPureDevice  flagCapacidadeDevice == CreateFlags.HardwareVertexProcessing) 
{ 
flagCapacidadeDevice = CreateFlags.PureDevice; 
Console.WriteLine("GameEngine.cs -> PureDevice"); 
} 
// Cria o Device. 
try 
{ 
this.device = new Device( 
inteiroAdapter, 
DeviceType.Hardware, 
this, 
flagCapacidadeDevice, 
presentParameters 
); 
return true; 
} 
catch (DirectXException) 
{ 
this.device = new Device( 
inteiroAdapter, 
DeviceType.Reference, 
this, 
CreateFlags.SoftwareVertexProcessing, 
presentParameters 
); 
return true; 
} 
} 

Note como agora temos um método muito mais robusto e completo para a inicialização de nosso Device. Este código ainda não trata todas as possibilidades mais é bastante útil e flexível. Não pense que em um jogo profissional teriamos uma função gigantesca para a criação do Device a toda hora. Configurações de execução são setadas antes de começarmos o negócio de nosso aplicativo. De pouco em pouco ensinarei novas técnicas e soluções para diversas situações que encontraremos no caminho.

Por hoje ficamos por aqui. Não se esqueça de continuar seus estudos pois a cada matéria, o nível técnico dos artigos tenderá a subir (assim como a dificuldade de utilização do framework). Então sem preguiça e bons estudos galera. Até a próxima!

“Vencer não é nada, se não se teve muito trabalho; fracassar não é nada se se fez o melhor possível.”

Nadia Boulanger