Post

Desenhando no Back Buffer

No artigo passado alocamos memória através do CreateDIBsection. Contudo, não usaremos mais ele.

Passo a passo

  1. Alocar memória com base em largura x altura x bytes por pixel.
  2. Armazenar em variável global as propriedades do bitmap.
  3. Stretch o tamanho do bitmap ao tamanho da janela.
  4. Atribuindo valores aos pixels dentro do loop.
  5. Extrair para uma função de lógica onde será preenchido os pixels (podemos dizer que é a função update() do jogo)
  6. No MainLoop atualiza constantemente os dados e renderiza (copy) o buffer.

Resumindo: cria a Janela -> tenta ler mensagens de input -> atualiza o bitmap -> renderiza o bitmap com os novos dados -> retorna a leitura de mensagens.

Bibliotecas comuns para inteiros

Para padronizar os inteiros, vamos usar a lib #include <stdint.h> e definir nossos próprios tipos.

1
2
3
4
5
6
7
8
9
10
11
#include <stdint.h>

typedef int8_t int8;
typedef int16_t int16;
typedef int32_t int32;
typedef int64_t int64;

typedef uint8_t uint8;
typedef uint16_t uint16;
typedef uint32_t uint32;
typedef uint64_t uint64;

Vamos alocar espaço de memória com o VirtualAlloc. Dessa forma, o Windows irá se encarregar de alocar no espaço físico. Por padrão a alocação acontece por páginas de 4096 bytes no mínimo.

1
2
3
4
5
6
7
8
9
10
11
internal void *bitmap_memory;
internal int bitmap_width;
internal int bitmap_height;
internal int bytes_per_pixel = 4;

int bitmap_memory_size = bitmap_width * bitmap_height * bytes_per_pixel;
bitmap_memory = VirtualAlloc(0, // start address (optional)
	bitmap_memory_size,
	MEM_RESERVE|MEM_COMMIT, // comitar e já começar a usar esse memória
	PAGE_READWRITE); // permissão

Para livrar espaço, vamos usar o VirtualFree.

1
2
3
4
5
6
7
8
9
10
11
if (bitmap_memory) {
    VirtualFree(&bitmap_memory, 0, MEM_RELEASE);
}

// o tamanho do bitmap será o tamanho da nova janela vinda do WM_SIZE.
bitmap_width  = width;
bitmap_height = height;

bitmap_info.bmiHeader.biSize   = sizeof(bitmap_info.bmiHeader); // size of bitmap header
bitmap_info.bmiHeader.biWidth  = bitmap_width;
bitmap_info.bmiHeader.biHeight = -bitmap_height; // negative - top-down left corner when rendering

Já no update_window(), vamos escalar o bitmap para ficar de acordo com a janela.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
internal void 
update_window(HDC context, RECT *client_rect, int x, int y, int width, int height) 
{
    int window_width  = client_rect->right  - client_rect->left;
    int window_height = client_rect->bottom - client_rect->top;

    StretchDIBits(context,
                  0, 0, window_width, window_height, // dest
                  0, 0, bitmap_width, bitmap_height, // src
                  bitmap_memory,
                  &bitmap_info,
                  DIB_RGB_COLORS,
                  SRCCOPY);
}

Criando Cores de Teste

Vamos preencher o bitmap com algumas cores. Para isso precisaremos do loop entre width x height x bytes_per_pixel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int pitch = width * bytes_per_pixel;
uint8 *row = (uint8 *) bitmap_memory; // usamos uint8 aqui pois no incremento com o pitch conseguimos seguir para próxima linha

for (int y = 0; y < height; ++y) {
    uint8 *pixel = (uint8 *) row; // acessando um canal por vez BB GG RR xx
    for (int x = 0; x < width; ++x) {
	    *pixel = 255;  // blue
		++pixel; 

	    *pixel = 0;
		++pixel; 

	    *pixel = 0;
		++pixel; 

	    *pixel = 0;
		++pixel; 
	}
	row += pitch;
}

Sempre que incrementamos um ponteiro, ele irá avançar o tamanho em bytes. Por exemplo, pixel++ avançará 1 byte porque pixel é um uint8.

A saída do código acima preencherá a tela com uma cor azul porque neste caso o Windows usa arquitetura Little Endian - BB GG RR xx.

Corrigindo o Main Loop

GetMessage é problemático. Ele sempre irá esperar para outra mensagem chegar na fila e usa o tempo da CPU (mas queremos usar o tempo da CPU só pra nós). PeekMessage fará a mesma coisa que o GetMessage que é receber mensagens, mas quando nao houver mensagens ele irá manter a execução ao invés de bloquear a thread.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
while(running) {
    Message message;
    // BOOL messageResult = GetMessage(&message, 0, 0, 0);
    
    // processa até a fila não ter mais mensagens.
    // Ou seja, vai consumindo os inputs como clicks, keyboard, exit close, etc
    // PM_REMOVE remove a mensagem da fila.
    while (PeekMessage(&message, 0, 0, 0, PM_REMOVE)) { 
    	if (message.message == WM_QUIT) {
    		running = false;
    	}
    
    	TranslateMessage(&message);
    	DispatchMessage(&message); // Dispatches a message to a window procedure
    }
}

Criando um Gradiente Estranho

Podemos agora alterar os valores de blue e green baseado nas coordenadas X e Y.

1
2
3
4
5
*pixel = (uint8) y;
++pixel;

*pixel = (uint8) x;
++pixel;

Como funciona cores

Como cada canal do pixel corresponde à 256 bits (1 byte), o resultado do desenho na tela será uma cor gradiente de 256 largura e 256 de height, resetando para zero sempre que o número de Y ou X ultrapassar 255 (overflow).

0 1 2 3 …. 255 0 1 2 3 …. 255

Vamos mover esse bloco para uma função com parâmetros para movimentar o eixo X e Y.

Nota: Desta vez, não vamos inserir 1 byte por vez. Agora vamos inserir diretamente 4 bytes com a ajuda dos operadores OR e SHIFT-LEFT << que irá colocar cada canal de uma vez só formando 24bits.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
internal void render_gradient(int x_offset, int y_offset) 
{
    int pitch = bitmap_width * bytes_per_pixel;
    uint8 *row = (uint8 *) bitmap_memory; 

    for(int y = 0; y < bitmap_height; ++y) {

        // uma escrita com 32bits é mais rápida que 8bits
        uint32 *pixel = (uint32 *) row; 
		
        for (int x = 0; x < bitmap_width; ++x) {
            uint8 b = (x + x_offset);
            uint8 g = (y + y_offset);
            *pixel = ((g << 8) | b);
            *pixel++;
        }
        row += pitch;
    }
    
}

Vamos a outro exemplo de atribuição de cores.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
*pixel = x; // B
++pixel;

*pixel = y; // G
++pixel;

*pixel = 0; // R
++pixel;

*pixel = 0; // x
++pixel;

// Para que possamos representar em 32 bits, 
// vamos definir primeiro os bits mais baixos (x) e depois, 
// usar o operador OU desses bits com os próximos 8 bits acima.

*pixel = ((y << 8) | x);

// Neste exemplo estou fazendo:

// [      OU  b   ]
// [   g  OU      ] ( aqui subi 8 bits com << )

Animando a Tela Com Pixels

Com o nosso main loop não-bloqueante, vamos animar os pixels fazendo com que o eixo X incremente a cada frame e seja atualizado pelo StretchDIBits.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int x = 0;
int y = 0;
while(running) {
    MSG message;
    while (PeekMessage(&message, 0, 0, 0, PM_REMOVE)) { // Fica renderizando e tentando buscar mensagens para dispachar.
    	if (message.message == WM_QUIT)
    	    running = false;
    
    	TranslateMessage(&message);
    	DispatchMessage(&message);
    } 
    
    // get window size
    RECT client_rect;
    GetClientRect(window, &client_rect);
    
    int window_width  = client_rect.right  - client_rect.left;
    int window_height = client_rect.bottom - client_rect.top;
    
    HDC context = GetDC(window); 
    
    // atualiza o jogo (bitmap)
    render_gradient(x, y);
    
    // aplica as mudanças
    update_window(context, &client_rect, 0, 0, window_width, window_height);
    
    ReleaseDC(window, context);
    
    x++;
}

Referências

Esta postagem está licenciada sob CC BY 4.0 pelo autor.