Animações em tempo real com a VCL - Parte II

No post anterior, fizemos uma introdução ao conceito de “Double buffer” e realizamos o tratamento da mensagem WM_ERASEBKGND para ajudar a otimizar um formulário VCL com o intuito de exibir animações de qualidade nele. Neste post, iremos finalizar estas otimizações.

Usando o evento OnIdle

O objeto Application está presente em todos os programas GUI do Delphi e é utilizado para simplificar a interação com a API do Windows. De fato, o objeto Application encapsula todas as chamadas à API necessárias para inicializar, registrar e manipular janelas, além de publicar alguns eventos globais.

O evento TApplication.OnIdle é um desses eventos. Ele é disparado sempre que o programa fica ocioso, ou seja, sempre que não houver mais nenhuma instrução a ser executada, o evento OnIdle será chamado. Isso o torna especialmente útil quando queremos realizar uma tarefa em segundo plano sem utilizar threads, ou quando queremos que uma determinada função seja executada repetidamente sem afetar o desempenho do programa.

Dois passos são necessários para utilizarmos o evento OnIdle.

Primeiro declare um procedimento privado em seu formulário e implemente-o como o código a seguir:

procedure OnIdle(Sender: TObject; var Done: Boolean);
begin
 Done := false;
end;

Note que não importa o nome do que você der ao método. O que importa é que a lista de parâmetros seja idêntica à do exemplo para que a assinatura do método seja compatível com a definição do evento Application.OnIdle para que, dessa forma, possamos associar nosso método ao evento.

Para finalizar, ponha o trecho de código abaixo no OnCreate do formulário e estaremos prontos para realizar nosso processamento neste evento.

procedure TfrmCGScreen.FormCreate(Sender: TObject);
begin
 Application.OnIdle := OnIdle;
end;

O loop principal (main loop)

Iremos utilizar o OnIdle para nosso loop principal.

O loop principal de uma aplicação de computação gráfica pode assumir várias formas, mas todas elas implementam variações da mesma lógica:

1. INICIO
2. ENQUANTO(ESTADO  <>  SAIR)
  2.1. PROCESSA OS COMANDOS DO USUÁRIO;
  2.2. ATUALIZA DO ESTADO DA ANIMAÇÃO;
  2.3. DESENHA A CENA NO BUFFER;
  2.4. EXIBE A CENA NO MONITOR;
3. FIM;

Vamos criar então os métodos equivalentes. Declare na seção privada do formulário os métodos a seguir e pressione Ctl + Shit + C para que o Delphi crie a implementação de todos:


procedure ProcessInput;    {passo 2.1}
procedure UpdateAnimation; {passo 2.2}
procedure DrawToBuffer;    {2.3}
procedure Blit;            {2.4}

Agora implemente o método OnIdle para que a estrutura do nosso algoritmo fique completa.

procedure TForm1.OnIdle(Sender: TObject; var Done: Boolean);
begin
 ProcessInput;
 UpdateAnimation;
 DrawToBuffer;
 Blit;
end;

Se você compilar e executar o programa agora, você verá uma janela do Windows vazia. Nada muito empolgante até agora: temos a estrutura geral pronta, mas nada pra exibir.

Vamos criar um objeto to TBall (adicione o arquivo uBall.pas ao seu projeto) no centro da tela. Declare uma variável pública chamada Ball e modifique o evento OnCreate do formulário para:

procedure TForm1.FormCreate(Sender: TObject);
begin
 ReportMemoryLeaksOnShutdown := True;

 {teremos uma tela de 800 x 600 pixels}
 ClientWidth := 800;              
 ClientHeight := 600;

 {Criamos nosso bitmap de buffer}
 fOffScreen := TBitmap.Create; 

 {iremos trabalhar com cores de 24bit}
 fOffScreen.PixelFormat := pf24bit;

 {configura as dimensões do buffer de acordo com as dimensões do formulário}
 fOffScreen.Width := ClientWidth;
 fOffScreen.Height := ClientHeight;
 Ball := TBall.Create;
 Ball.Position:= Point(ClientWidth div 2, ClientHeight div 2);
 Ball.Color := clWhite;
 Ball.Diameter := 30;
 Application.OnIdle := OnIdle;
end;

Agora é preciso exibir o objeto. Vamos implementar os métodos DrawToBuffer e Blit:

procedure TForm1.DrawToBuffer;
var
 Canvas : TCanvas;
begin
 Canvas := fOffScreen.Canvas;

 {Primeiro, limpamos o buffer com um fundo preto...}
 Canvas.Brush.Color := clBlack;

 {Agora desenhamos a bola}
 Canvas.FillRect(fOffScreen.Canvas.ClipRect);
 Ball.Draw(Canvas);
end;

procedure TForm1.Blit;
begin
 BitBlt(Canvas.Handle, 0, 0, ClientWidth, ClientHeight,
        fOffScreen.Canvas.Handle, 0, 0, SRCCOPY);
end;

Com isto temos, finalmente, nosso framework funcionando. Tudo bem que ele não faz mais nada além de desenhar uma bola no meio da tela, mas o “trabalho sujo” de configuração do ambiente foi realizado.
Para finalizar este post, vamos movimentar um pouco a bola pela tela usando as setas do teclado.

Implemente o método ProccessInput usando a função GetKeyState como na listagem abaixo. Note que os botões + e - irão manipular a velocidade em que a bola se movimenta.

procedure TForm1.ProcessInput;
begin
 if GetKeyState(VK_UP) then
    Ball.Move(0,-1);

 if GetKeyState(VK_DOWN) then
    Ball.Move(0, 1);

 if GetKeyState(VK_LEFT) then
    Ball.Move(-1, 0);

 if GetKeyState(VK_RIGHT) then
    Ball.Move(1, 0);

 if GetKeyState(VK_SUBTRACT) then
    Ball.Speed := Ball.Speed - 0.1;

 if GetKeyState(VK_ADD) then
    Ball.Speed := Ball.Speed + 0.1;
end;

Compile o projeto e experimente um pouco. Note que a bola não ultrapassa os limites da tela e que a classe TBall é responsável por este tratamento.

No próximo post, iremos adicionar um fundo com animação iremos expandir a classe TBall para trabalhar com sprites animados.

Até o próximo post.

Downloads

Links

Links para as postagens que fazem parte do mini curso "Animações em Tempo real com a VCL"

Animações em tempo real com a VCL - Parte I

O Delphi facilita muito a construção de interfaces gráficas. Arrastando componentes da barra de ferramentas e alterando suas propriedades pelo “object inspector”, é muito fácil construir uma interface consistente para a maioria das aplicações.

Esta abordagem, porém, não pode ser utilizada para construir uma superfície de desenho adequada para a exibição de animações que exijam uma taxa de atualização aceitável (pelo menos 20 quadros por segundo). Para isso, a solução mais óbvia seria utilizar uma API ou framework projetado especificamente para este fim (como OpenGL ou DirectX).

 Existem, porém, dois problemas a ser vencidos para adotar um framework destes. O primeiro problema é que, caso não haja um hardware gráfico moderno disponível na máquina onde a aplicação irá ser executada, nenhuma aceleração de será provida (leia-se: todo o processamento gráfico será feito por software). O segundo problema é a curva de aprendizado, consideravelmente longa, que o programador terá de vencer para conseguir aproveitar os recursos que elas possam oferecer.

Então, qual biblioteca utilizar? Não dá pra fazer isso com a própria VCL?

Sim. Dá sim. Mas algumas otimizações precisam ser feitas. E é sobre estas otimizações que vamos falar.

Double Buffer ou Backbuffer

O TCanvas representa uma superfície de desenho que pode ser entendida e tratada como um array de pixels ou um buffer de pixels. O Windows mapeia este buffer para a memória de vídeo e então a placa de vídeo trata de interpretar esta informação e convertê-la em uma imagem visível no monitor.

Se desenharmos direto num TCanvas de um objeto visível, o desenho será imediatamente atualizado na tela. Até aí, tudo bem. Desde que sejam desenhados um ou dois objetos por vez, tudo irá ser atualizado rápido o suficiente para que a transição não seja facilmente percebida.

Ponha um TTimer num form, configure a propriedade interval para 20, carregue uma imagem qualquer num TImage e ponha o seguinte código no evento OnTimer:

procedure TForm1.Timer1Timer(Sender: TObject);
begin
  Canvas.Brush.Color := clWhite;
  Canvas.FillRect(Canvas.ClipRect);
  Canvas.Draw(X, Y, Image1.Picture.Graphic);
  Inc(X);
  Inc(Y);
  if X >= ClientWidth - Image1.Width then
     X := 0;
  if Y >= ClientHeight - Image1.Height then
     Y := 0;
end;

Ao executar este programa, você verá sua imagem sendo redesenha aproximadamente 50 vezes por segundo (1000/20=50), o suficiente para causar a impressão de movimento, mas a imagem fica piscando constantemente e o resultado é realmente muito feio.

A idéia geral do buffer duplo (Double buffer) é ter uma segunda área de desenho que fique sempre invisível. Daí, em vez de desenhar direto na tela, todos os desenhos seriam gerados neste buffer e, somente depois de pronto, copiaríamos o desenho para a área visível da tela, numa única operação.

A primeira impressão que essa idéia passa é de que, como estaremos percorrendo um caminho maior, as coisas ficarão mais lentas. Mas o fato é que, quando desenhamos num TCanvas visível, a operação demora mais porque, além do tempo gasto com o desenho em si, o programa tem que esperar a memória de vídeo ser atualizada para que o as mudanças apareçam na tela e o programa possa seguir. Quando desenhamos na memória (num TCanvas não visível) este atraso não existe, logo, a operação é concluída muito mais rápido. Além disso, quando vamos exibir a imagem na tela, ela já está pronta e somente uma operação de desenho será realizada nesta “área lenta”, o que contribui para que a animação tenha uma aparência mais agradável.
Vejamos como implementar esta idéia em Delphi.

Primeiro, declare uma variável privada do tipo TBitmap para ser o nosso “back buffer” e trate a inicialização e destruição deste objeto:

private
  { Private declarations }
  fOffScreen: TBitmap;

(...)

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  FreeAndNil(fOffScreen);
end;

(...)

procedure TForm1.FormCreate(Sender: TObject);
begin
  X := 0;
  Y := 0;
  fOffScreen := TBitmap.Create;
  fOffScreen.PixelFormat := pf24bit;
  fOffScreen.Width := ClientWidth;
  fOffScreen.Height := ClientHeight;
end;

Agora vamos desenhar na memória e, quando concluído o desenho, vamos transferi-lo para a “área visível” da tela.

procedure TForm1.Timer1Timer(Sender: TObject);
begin
  fOffScreen.Canvas.Brush.Color := clWhite;
  fOffScreen.Canvas.FillRect(fOffScreen.Canvas.ClipRect);
  fOffScreen.Canvas.Draw(X, Y, Image1.Picture.Graphic);

  BitBlt(Canvas.Handle, 0, 0,
         ClientWidth,
         ClientHeight,
         fOffScreen.Canvas.Handle,
         0, 0, SRCCOPY);
  Inc(X);
  Inc(Y);
  if X >= ClientWidth - Image1.Width then
     X := 0;
  if Y >= ClientHeight - Image1.Height then
     Y := 0;
end;

Execute este programa e veja que o problema da imagem piscando (conhecido como flickering) foi resolvido.

Eliminando interferências do Windows

Outra otimização mais sutil, mas não menos importante, é o tratamento adequado da mensagem WM_ERASEBKGND que o Windows envia ao programa quando ele acha que alguma área da janela deve ser atualizada.

O Delphi, por padrão, em resposta a esta mensagem irá invalidar toda a área cliente da janela, forçando um redesenho completo. Este comportamento faz sentido quando deixamos que o Delphi cuide sozinho do desenho e da atualização da janela, mas no nosso caso, queremos ter controle completo sobre o modo que a janela será exibida.

Vamos criar uma procedure que irá realizar o tratamento desta mensagem. Declare o seguinte procedimento :

procedure WMEraseBg(var Msg: TWMEraseBkgnd); message WM_ERASEBKGND;

E em sua implementação diga ao Windows, que você mesmo irá tratar do redesenho da janela, setando a mensagem MSG para 0:

procedure TfrmCGScreen.WMEraseBg(var Msg: TWMEraseBkgnd);
begin
  Msg.Result := 0;
end;

Um template para animações

Com estas duas otimizações implementadas, já dá pra fazer pequenas animações executarem de maneira eficiente. Mas ainda há espaço para outras otimizações (que trataremos no próximo post), como executar o programa em full screen, substituir o TTimer por funções mais precisas como QueryPerformaceCounter e utilizar o evento OnIdle do TApplication para realizar os desenhos da animação.

Neste link, você pode baixar os fontes de um projeto que implementa todas estas otimizações e utilizá-lo como template para seus próprios projetos. Faça o download do arquivo, abra-o no Delphi, clique com um botão direito em cima do form e escolha a opção “Add to Repository”. Pronto. Este template estará disponível no “Object Repository”!

Para usar o template, clique no menu File > New > Other, escolha o formulário adicionado anteriormente, marque a opção “inherit” e sobrescreva o método DrawScene para criar suas animações.

Até o próximo post.

Downloads


Links

Links para as postagens que fazem parte do mini curso "Animações em Tempo real com a VCL"

Sistemas de partículas

Segundo a dinâmica das partículas, um sistema de partículas é o conjunto de leis físicas que regem um conjunto de n partículas partindo de um centro de massa ou emissor. Muitos fenômenos podem ser modelados como sistemas de partículas. A chuva (cada gota é uma partícula e o emissor é o conjunto de nuvens de onde as gotas vêm), a neve (cada floco uma partícula e o emissor, o ponto de condensação/cristalização do vapor de água) o fogo (cada chama é uma partícula e o emissor é o material sendo queimado), todos estes podem ser descritos como tal.

Um sistema simples utilizando vetores bidimensionais para calcular a o deslocamento das partículas pode ser facilmente modelado no Delphi e exibido num TCanvas padrão. Neste sistema (veja os links abaixo) é possível manipular a força do vento, a força da gravidade e o centro de massa de maneira interativa e ver em tempo real o resultado na tela.
Este sistema de exemplo se distancia da realidade física por não calcular desvios de rota resultantes de colisões entre partículas e implementar somente duas grandezas vetoriais (a gravidade e o vento) quando no mundo real o número de grandezas envolvidas num sistema de partículas é muito maior. Além disso, cada partícula percorre uma trajetória linear, o que nem sempre é condizente com o sistema que se está tentando simular.

Mesmo com todas essas omissões, é possível conseguir efeitos especiais atraentes e leves para serem usados em jogos. Este exemplo faz uso das rotinas gráficas da API do Windows, portanto, não requer nenhum componente ou biblioteca especial para compliar.

Para controlar a gravidade, utilize as teclas "+" e "-" do teclado numérico. Para controlar a direção e a força do vento, use as setas direcionais.

Evoé...


Código fonte
Executável