segunda-feira, 13 de outubro de 2014

CG: 3D sem OpenGL ou DirectX – Parte 1/4

Vou começar hoje uma série de artigos - quatro posts, acredito eu - sobre programação de gráficos 3D sem a ajuda de OpenGL ou DirectX. Muita gente talvez nem pense neste tipo coisa hoje em dia, mas no início dos tempos dos gráficos 3d, tudo era feito à mão. Todas as API’s ainda precisavam ser escritas e a reusabilidade de código era praticamente zero. A internet não possuía muito conteúdo e grande parte dos programadores viviam isolados em seus computadores em companhia de livros de exatas e um monte de datasheets de placas com recursos mirabolantes fornecidos pelos fabricantes de hardwares que nem sempre tentavam facilitar muito as coisas.

J. Willard Marriott Library's Special Collections


Hoje em dia, do contrário, temos bastante informação sobre uma infinidade de coisas e várias APIs que nos fornecem a abstração de tudo que não precisamos saber. A padronização de recursos e chamadas, além da reusabilidade de código, tornou a vida mais fácil, a produção mais objetiva e eficiente, e o resultado muito melhor.

Então você deve estar se perguntando, por que aprender algo que já foi desenvolvido e hoje é abstraído por bibliotecas e APIs gráficas, quando podemos aprender OpenGL ou DirectX e conseguir resultados muito melhores? Utilizar OpenGL e DirectX é sem dúvida o mais indicado hoje em dia, mas conhecer seus conceitos internos, nunca é demais. O objetivo deste artigo é puramente o de esclarecimento, entender como as coisas funcionavam ou funcionam por trás dos bastidores, pode nos trazer benefícios no desenvolvimento de novos recursos, ou mesmos nos esclarecer como funcionam os mecanismos que foram utilizados para a construção do que se tem hoje.

Preparando o terreno


Iremos utilizar para esta série de artigos, a linguagem C sempre procurando manter o código o mais simples possível. Utilizaremos também a biblioteca SDL2 e a IDE do Code::Blocks. Escrevi um post com o título “Configurando SDL2 no Code::Blocks 13”, onde demostro como configurar o ambiente que iremos utilizar aqui para se trabalhar com a biblioteca SDL2. Você pode escolher o ambiente de desenvolvimento que achar melhor, assim como a linguagem e a API para os recursos básicos de desenho, isto não irá interferir no resultado final.
 
Nesta primeira parte, começaremos a construir uma aplicação que será a base para darmos seguimento aos próximos artigos até conseguirmos alguma coisa em três dimensões. Criaremos uma janela com uma camada de render e uma textura, que iremos aplicar ao render. Pintaremos a textura através do acesso a um vetor de inteiros de 32 bits, sendo composto pelos canais Alfa, Red, Green e Blue (ARGB) com 8 bits cada. Para quem não está familiarizado com os conceitos de cores em computadores, sugiro pesquisar um pouco na internet antes de dar seguimento.

Por se tratar de um “3D old time”, não utilizaremos os recursos de desenhos que a SDL2 nos oferece. Desenvolveremos nossas funções para desenho, a começar pela função de desenho de linhas, utilizando o algoritmo de Bresenham. Se optarem por utilizar outra linguagem ou API, basta substituírem o acesso através da SDL2, por um bitmap e pintá-lo na camada de apresentação da janela.

Segue a listagem do código de exemplo. O aplicativo irá desenhar 500 linhas coloridas de forma aleatória. A explicação do código vem logo a seguir:

#include <stdio.h>
#include <stdlib.h>
#include "SDL2/SDL.h"

#define MAKECOLOR(a,r,g,b) ((a << 24) | (r << 16) | (g << 8) | b)

void drawPixel(unsigned long *surface, int x, int y, unsigned long color);
void drawLine(unsigned long *surface, int x1, int y1, int x2, int y2, unsigned long color);

int main(int argc, char **argv)
{
    int quit = 0, i;
    SDL_Event event;

    SDL_Init(SDL_INIT_VIDEO);

    SDL_Window *window = SDL_CreateWindow("Blog QuaseHard - 3D sem OpenGL ou DirectX", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 640, 480, 0);
    SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_SOFTWARE);
    SDL_Texture *surface = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, 640, 480);

    unsigned long *pixels = (unsigned long*) malloc(640 * 480 * sizeof(unsigned long));
    memset(pixels, 0, 640 * 480 * sizeof(unsigned long));

    srand(time(NULL));

    for (i=0;i<500;i++)
    {
        int x  = rand() % 640;
        int x2 = rand() % 640;
        int y  = rand() % 480;
        int y2 = rand() % 480;

        drawLine(pixels, x, y, x2, y2, MAKECOLOR(0,rand()%255,rand()%255,rand()%255));
    }

    while (!quit)
    {
        SDL_UpdateTexture(surface, NULL, pixels, 640 * sizeof(unsigned long));
        SDL_WaitEvent(&event);

        switch (event.type)
        {
            case SDL_QUIT:
                quit = 1;
                break;
        }

        SDL_RenderClear(renderer);
        SDL_RenderCopy(renderer, surface, NULL, NULL);
        SDL_RenderPresent(renderer);
    }

    free(pixels);

    SDL_DestroyTexture(surface);
    SDL_DestroyRenderer(renderer);

    SDL_DestroyWindow(window);
    SDL_Quit();

    return 0;
}

void drawPixel(unsigned long *surface, int x, int y, unsigned long color)
{
    x = (x<0) ? x = 0: x;
    y = (y<0) ? y = 0: y;
    x = (x>=640) ? x = 639: x;
    y = (y>=480) ? y = 479: y;

    surface[y * 640 + x] = color;
}

void drawLine(unsigned long *surface, int x1, int y1, int x2, int y2, unsigned long color)
{
    /*Algoritmo de BRESENHAM*/

    int dx, dy, i, e;
    int incx, incy, inc1, inc2;
    int x, y;

    dx = x2 - x1;
    dy = y2 - y1;

    if(dx < 0) dx = -dx;
    if(dy < 0) dy = -dy;

    incx = 1;

    if(x2 < x1) incx = -1;

    incy = 1;

    if(y2 < y1) incy = -1;

    x = x1;
    y = y1;

    if(dx > dy)
    {
        drawPixel(surface, x, y, color);
        e = 2*dy - dx;
        inc1 = 2*( dy -dx);
        inc2 = 2*dy;
        for(i = 0; i < dx; i++)
        {
            if(e >= 0)
            {
                y += incy;
                e += inc1;
            }
            else e += inc2;

            x += incx;
            drawPixel(surface, x, y, color);
        }
    }
    else
    {
        drawPixel(surface, x, y, color);
        e = 2*dx - dy;
        inc1 = 2*( dx - dy);
        inc2 = 2*dx;
        for(i = 0; i < dy; i++)
        {
            if(e >= 0)
            {
                x += incx;
                e += inc1;
            }
            else e += inc2;

            y += incy;
            drawPixel(surface, x, y, color);
        }
    }
}
 

Explicação do código


O código começa pela macro define que criamos para transformar as componentes dos canais ARGB em um inteiro longo de 32 bits seguido pelo protótipo das duas funções para desenho dos pixels e das linhas. Falaremos delas mais tarde.

Na função main(), criamos uma variável event que é um union que contém a estrutura para diferentes tipos de eventos lançados pela SDL2 durante o funcionamento da aplicação. É através deste que controlamos todos os eventos ocorridos na janela do programa.

SDL_Window *window = SDL_CreateWindow("Blog QuaseHard - 3D sem OpenGL ou DirectX", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 640, 480, 0);
SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_SOFTWARE);
SDL_Texture *surface = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, 640, 480);

Através da chamada à função SDL_CreateWindow, a SDL2 irá criar uma janela para a aplicação. A função SDL_CreateRenderer é chamada para criar um contexto de renderização 2d. Já a função SDL_CreateTexture é utilizada para criar uma textura para o contexto de renderização. Como pode ser observado, passamos o argumento SDL_PIXELFORMAT_ARGB8888 para a função SDL_CreateTexture. Existem outros formatos possíveis que podem ser vistos na documentação da SDL2. Como dito anteriormente, trabalharemos com um formato de 32 bits para cada pixel na tela, sendo 8 bits para cada canal, Alfa, Red, Green e Blue (ARGB). Desta forma podemos trabalhar com uma infinidade de cores para cada pixel, que terá um valor de 0 a 255 de variação de cor possível em cada canal. Para o tipo de acesso, escolhemos o SDL_TEXTUREACCESS_TARGET. De acordo com a documentação, pode ser utilizado como alvo da renderização.

Com a definição de nossa textura com 32 bits, precisamos agora definir nosso vetor de pixels, que irá conter a informação de cores e posição que iremos pintar em nossa textura para copiarmos para o ambiente do render da SDL. Criamos um ponteiro para um local de memória alocado pela função malloc() com o tamanho de nossa tela, 640 x 480 x 32bits:
unsigned long *pixels = (unsigned long*) malloc(640 * 480 * sizeof(unsigned long));
memset(pixels, 0, 640 * 480 * sizeof(unsigned long));

A próxima etapa foi gerar as linhas aleatórias, como teste do funcionamento de nossa rotina de desenho. O código irá gerar 500 linhas em posições e cores aleatórias dentro de nosso vetor de pixels antes de entrar no loop principal do programa.

No loop principal, fazemos uma chamada à SDL_UpdateTexture, que é responsável por atualizar a textura com a informação da nossa variável de pixels. Neste primeiro momento, não havia a necessidade de se alocar esta função no loop visto que a informação em pixels não é alterada durante o funcionamento do programa, mesmo assim, como futuramente teremos uma aplicação dinâmica, onde o valor de pixels será alterado à todo momento, optei por mantê-la já neste momento dentro do loop.

A função SDL_WaitEvent é chamada para obter os eventos que são capturados pela SDL2. Para informações sobre os tipos de eventos, consulte a documentação da SDL2 no site do desenvolvedor.
Ainda dentro do loop, testamos se o evento é o fechamento da janela, se sim, finalizamos o programa alterando a variável de controle do loop.

Dentro do loop, ainda realizamos a chamada para as funções:
SDL_RenderClear(renderer);
SDL_RenderCopy(renderer, surface, NULL, NULL);
SDL_RenderPresent(renderer);

Estas funções são responsáveis pela apresentação do conteúdo na janela de exibição da SDL2. Limpamos a área de render, copiamos a textura com os pixels para a mesma e apresentamos na tela o resultado à cada iteração do loop.

Quando saímos do programa, liberamos a memória que alocamos em pixels, destruímos a textura e a área de render, assim como a janela que foi criada e finalizamos o ambiente da SDL através das chamadas:
free(pixels);
SDL_DestroyTexture(surface);
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();

Foram criadas mais duas funções neste código, drawPixel() e drawLine(). A função drawPixel() escreve diretamente em nosso vetor de pixels na memória, na posição x e y passadas, o valor de 32 bits que foi passado como valor pela variável color. Como temos um vetor de 640 x 480, ou seja, 307200 posições, precisamos multiplicar o valor de y por 640, que é o comprimento total de uma linha na renderização, e somar o valor de x.

Como exemplo, se a largura X total de nossa tela fosse de 10 px e a altura Y de 10 px e quiséssemos pintar um pixel amarelo (valor = 15 somente para representação) nas posições x = 5; y = 0 e x = 8; y = 1; teríamos algo parecido com isso na memória:


Que seria a representação de algo como:
pixel[ 0 * 10 + 5 ] = 15; //posição 5 do vetor.
pixel[ 1 * 10 + 8 ] = 15; //posição 18 do vetor.

Geralmente, a memória de vídeo é um vetor sequencial na memória do computador, e não uma matriz de duas posições. Basicamente, é desta forma que as coisas funcionam por trás dos bastidores quando você chama uma rotina da API para plotar um pixel em uma tela.

Já a função drawLine(), foi criada com base no algoritmo desenvolvido por Jack Elton Bresenham em 1962 na IBM, de acordo com a Wikipedia. O algoritmo de Bresenham para linhas, é um método para traçar retas entre dois pontos em um plano cartesiano x e y, incrementando as coordenadas dependendo da diferença entre a posição inicial e final. Ele faz isso através do cálculo de erro baseado na menor diferença que é somada até que seja superior à outra diferença, fazendo com que esta outra diferença, seja incrementada.

Para informações em português acesse este link onde se encontra mais detalhes sobre este algoritmo.

O primeiro resultado


O resultado deste programa pode ser visto pela imagem abaixo, com a janela SDL pintada por 500 retas com direções e cores aleatórias:

Janela com o resultado das 500 linhas com posições e cores aleatórias.
Nesta primeira parte do tutorial, criamos um ambiente para dar inicio à nossa aplicação 3D. O que foi feito aqui foi bem simples e pode ser implementado sem nenhum problema em qualquer linguagem ou ambiente gráfico. A escolha pela utilização da SDL2 foi apenas para manter o foco no desenvolvimento das rotinas para desenho, sem precisar entrar em detalhes aprofundados de outras APIs, visto que SDL2 nos fornece acesso direto para pintar na tela sem grandes esforços.

Espero que tenham gostado e vamos seguir com a próxima parte vendo um pouco de matrizes.

Breno.
Compartilhe:

0 comentários:

QuaseHard. Tecnologia do Blogger.