Introdução ao OpenGL usando SDL – parte 1

Nesse artigo, iremos cobrir a básico da maravilhosa api gráfica OpenGL. Que fique bem claro para começo de conversa que o OpenGL é um tópico bem extenso para se cobrir em apenas um tutorial. Por isso, esse artigo simplesmente será uma introdução ao assunto. Mas, como em todos os HOW-TOs, algumas coisas são esperadas do leitor. Você deve entender de C++, SDL e alguns conceitos de matemática (geometria/álgebra, nada muito complexo). Dito isso, vamos começar…

O OpenGL é o que conhecemos como”máquina de estados”. Ele essencialmente contém várias flags que você irá aprender de modo a manipular a saída do programa. Para a maior parte das coisas, quando você ajusta uma flag, ela irá permanecer com esse valor até ser alterada. Uma vez que você tenha pego a manha das coisas, o OpenGL permite que você descreva sem esforço uma cena 3D (incluindo texturas/iluminação/fumaça…Todo tipo de coisas do tipo). Em primeiro lugar, vamos analisar um código simples. Ao longo desse tutorial, estaremos adicionando coisas a ele.

 

#include <stdlib.h>
#include <GL/gl.h>
#include <GL/glu.h>
#include <SDL/SDL.h>

using namespace std;

// Width & Height of window
#define width 640
#define height 480

// Kill program
void endProgram(int code) {SDL_Quit();    exit(code);}

// Handle SDL keypresses
void handleKeys(SDL_keysym* keysym, bool state) {
    switch(keysym->sym) {
        case SDLK_ESCAPE:    endProgram(0); break;
    }
}

// Process SDL events
void processEvents() {
    SDL_Event event;
    while(SDL_PollEvent(&event)) {
        switch(event.type) {
            case SDL_KEYDOWN:    handleKeys(&event.key.keysym, true ); break;
            case SDL_KEYUP  : handleKeys(&event.key.keysym, false);    break;
            case SDL_QUIT   : endProgram(0); break;
        }
    }
}

void mainLoop() {
    while(true) {
        processEvents();

        glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); // Clear color and depth buffer
        glLoadIdentity(); // Reset orientation

        // Graphical commands go here

        SDL_GL_SwapBuffers(); // Update screen

    }
}

// Setup OpenGL perspective
void setupOpengl() {
    glViewport(0, 0, width, height);
    glMatrixMode(GL_PROJECTION);
    glEnable(GL_DEPTH_TEST);
    gluPerspective(45, (float)width/height, .1, 100);
    glMatrixMode(GL_MODELVIEW);
}

// Init everything
int main(int argc, char* argv[]) {
    SDL_Init(SDL_INIT_VIDEO);
    SDL_SetVideoMode(width, height, 24, SDL_OPENGL | SDL_GL_DOUBLEBUFFER);
    setupOpengl();
    mainLoop();
    return 0;
}

 

Agora vamos tentar entender esse código partindo de baixo. A função “main()” é a parte mais baixa, os comandos contidos nela simplesmente inicializam a janela SDL o OpenGL ativado. Depois, chama “setupOpengl”, que é provavelmente a função mais assustadora.  Infelizmente, esse é um dos pontos complexos demais para serem explicados aqui, mas precisamos aprender o básico. Então… tente me acompanhar nisto porque tudo que ela faz é configurar as perspectiva do opengl de forma que possamos renderizar tudo de forma adequada em 3D. Em seguida chamamos a função “mainLoop”. Essa função contém um loop infinito. A primeira coisa de loop é uma função chamada “processEvents”. Tudo que ela faz nesse ponto é se certificar que a janela SDL não foi fechada (e checa pela tecla Esc para fecha-la). Em seguida, podemos ver “glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFR_BIT)”, que é um comando bem grande. Pense nele como um método que limpa a tela.

Em seguida temos “glLoadIdentity” que simplesmente reseta nossa orientação… Veja você, mais tarde estaremos rotacionando e movendo as coisas pela tela e essa função reseta todas as rotações e translações. Sem ela, as coisas poderiam ficar bem bagunçadas rapidamente. Em seguida você lerá “Graphical commands go here”, que é onde estaremos inserindo algumas coisas legais em instantes, mas nesse momento apenas fique com esse local em mente. Por último, “SDL_GL_SwapBuffers”, que é o comando que de fato renderiza a cena 3D na tela; sem elam nunca seriamos capazes de ver o que nós desenhamos. A razão para isso se deve porque n~ao desenhamos nada diretamente na tela, se fizermos isso, você veria todos os objetos individuais sendo desenhados e tudo pareceria muito agitado. Ao invés disso, renderizamos tudo em um “back-buffer” e uma vez que tudo estiver pronto, carregamos essa buffer na tela. Agora que você já tem um idéia  do que o programa faz, compile-o com ” g++ -lSDL -lGLU -lGL myProgram.cpp”.

Ok, com nossa implementação OpenGL pronta para usar, vamos direto aos negócios, A primeira coisa que você precisa entender é o sistema de coordenadas. Como você deve saber, um programa 3D requer 3 valores para cada ponto do espaço 3D.O X, o Y e o Z. Toda vez que você desenhar no OpenGL, precisaremos definir esses três pontos para expressar ao OpenGL onde no espaço 3D queremos desenhar. Agora, precisaremos de um frame de referência então vá em frente e compile e execute o código acima. Você vê uma tela em branco? Bom! Agora pegue o seu dedo e coloque-o bem no meio. Exatamente onde a ponta do dedo está, fica localizada o ponto (0,0,0), ou seja, o ponto onde os valores (X,Y,Z) são todos 0. O centro de nosso sistema de coordenadas. Agora, mantendo o dedo no meio da janela…

Se você mover seu dedo para a esquerda, os valores de X serão negativos.

Se você mover seu dedo para a direita, os valores de X serão positivos.

Movendo o seu dedo para baixo, o resultado será um valor negativo para Y.

Movendo para cima, temos um valor positivo para Y.

Se você empurrar o seu dedo para dentro de seu monitor (por favor, não faça isso), os valores de Z seriam negativos.

Se você trouxer o seu dedo para perto de você, os valores de Z serão positivos.

Agora você tem um bom entendimento da orientação do sistema de coordenadas. Para testar iremos renderizar um único ponto no espaço 3D. Na linha com o comentário “Graphical commands g0 here” você deve substituir esse comentário com o código abaixo…

        glBegin(GL_POINTS);
        glVertex3f(0,0,-1);
        glEnd();

Se tudo correr bem e você compilar e executar esse código, deve ver um ponto onde o seu dedo estaria. Bem simples, não? Agora vamos explicar os comandos: “glBegin(GL_POINTS)”  diz ao OpenGl que estamos para desenhar alguns pontos (obviamente), “glVertex3f(0,0,-1)” diz ao OpenGl que iremos colocar um vórtice (que é basicamente um ponto no espaço 3D) na posição (0,0,-1), e por último “glEnd()” diz ao OpenGl que terminamos o desenho dos pontos. Esperançosamente, isso faz sentido, mas você pode estar se perguntando foi usada a posição (0,0,-1) ao invés de (0,0,0). A resposta é bem simples: o ponto (0,0,0) estaria diretamente sobre a tela e queremos desenhar nossos objetos por trás da tela de forma que configuramos uma unidade. Ok, agora iremos desenhar alguns poucos pontos apenas para garantir que tudo esteja entendido. Substitua o código digitado pelo seguinte:

        glBegin(GL_POINTS);
        glVertex3f( 0, 1,-10);
        glVertex3f(-1,-1,-10);
        glVertex3f( 1,-1,-10);
        glEnd();

Como você pode ver, tudo que temos que fazer foi adicionar comandos “glVertex3f” para criar mais pontos. Mas porque criamos os pontos com 10 unidades ao invés de 1 desta vez? Perspectiva, esse é o motivo! Imagine se um de seus amigos colocou um bloco na rua e acene os braços … Capturou essa imagem? Ok, agora acene seus braços em frente de seu rosto. WOW, que diferença na quantidade de movimento. Observe como seus braços são capazes de sair do seu campo de visão por estarem tão perto, enquanto os braços de seu amigo  fazem arcos completos sem sair de vista? É o mesmo conceito, movemos os objetos mais para trás no eixo Z de forma que podemos move-los nos eixos X e Y sem tira-los da tela. Tente alterar esses valores -10 para -20. Reparou como os pontos aparecem muito mais próximos juntos?

Ok, esses desenhos de pontos são meio chatos. E se quisermos desenhar um forma? Fácil. Vamos conectar esses pontos para formar um triângulo:

        glBegin(GL_TRIANGLES);
        glVertex3f( 0, 1,-10);
        glVertex3f(-1,-1,-10);
        glVertex3f( 1,-1,-10);
        glEnd();

Isso está certo, esses mesmos vértices que eram apenas pontos, podem formar um triângulo apenas pela mudança de “GL_POINTS” para “GL_TRIANGLES”.

E para desenhar um quadrado? Bem, quadrados em  OpenGl são conhecido como QUADS, cheque o código abaixo:

        glBegin(GL_QUADS);
        glVertex3f(-1, 1,-10);
        glVertex3f( 1, 1,-10);
        glVertex3f( 1,-1,-10);
        glVertex3f(-1,-1,-10);
        glEnd();

Sim, apenas adicione outro vértice e altere de “GL_TRIANGLES” para “GL_QUADS”.

Agora que você está familiar com o “glVeretx3f”, vamos ver mais um comando bem similar chamado “glColor3f”.

        glBegin(GL_QUADS);
        glColor3f ( 0, 0,  1);
        glVertex3f(-1, 1,-10);
        glVertex3f( 1, 1,-10);
        glVertex3f( 1,-1,-10);
        glVertex3f(-1,-1,-10);
        glEnd();

“glColor3f” é usado dessa forma: “glColor3f(Vermelho, Verde, Azul)” onde Vermelho/Verde/Azul são substituídos por um valor entre 0 e 1 dependendo de quanto da cor você gostaria de usar. Esse comando também demonstra o que significa o que significa quando foi dito “Máquina de estados”. Uma vez que tivermos configurado a cor para azul, todos os objetos desenhados após isso serão desenhados azul. O que acontece quando trocamos as cores entre vértices? Tente para ver.

        glBegin(GL_QUADS);
        glColor3f(1,0,0); glVertex3f(-1, 1,-10);
        glColor3f(1,1,0); glVertex3f( 1, 1,-10);
        glColor3f(0,1,0); glVertex3f( 1,-1,-10);
        glColor3f(0,0,1); glVertex3f(-1,-1,-10);
        glEnd();

Observou como o OpenGl interpolou as cores entre cada vértice? bem legal de ver, não? Então, recapitulando: vimos o uso de “glBegin” usando GL_POINTS, GL_TRIANGLES e GL_QUADS, como colocar vértices onde queremos usando glVertex3f e como colorir nossos objetos usando glColor3f.

Agora iremos partir para coisas mais interessantes: Translação e rotação. A primeira coisa da lista é translação. Tudo que é feito aqui é mudar o sistema de coordenadas na direção que nós especificamos.  Por quê faríamos isso? Porque é muito mais fácil que alterar cada posição de vértice manualmente; podemos apenas alterar o sistema de coordenadas inteiro, como nesse exemplo:

        glTranslatef(-2, 0, 0);
        glBegin(GL_QUADS);
        glColor3f(1,0,0); glVertex3f(-1, 1,-10);
        glColor3f(1,1,0); glVertex3f( 1, 1,-10);
        glColor3f(0,1,0); glVertex3f( 1,-1,-10);
        glColor3f(0,0,1); glVertex3f(-1,-1,-10);
        glEnd();

Observe como o quadrado está no lado esquerdo agora, porque movemos o sistema de coordenadas -2 unidades sobre o eixo X. Uma coisa legal sobre isso que iremos mostrar a seguir é que podemos transladar o quadrado -10 pontos sobre o eixo Z, de forma que não precisaríamos de todos esses -10 pontos. Abaixo um exemplo:

        glTranslatef(0, 0, -10);
        glBegin(GL_QUADS);
        glColor3f(1,0,0); glVertex3f(-1, 1, 0);
        glColor3f(1,1,0); glVertex3f( 1, 1, 0);
        glColor3f(0,1,0); glVertex3f( 1,-1, 0);
        glColor3f(0,0,1); glVertex3f(-1,-1, 0);
        glEnd();

Outra coisa sobre o OpenGl que veremos agora é que uma vez que algo tiver sido desenhado, executar uma translação não afetará ele. Apenas serão afetados os objetos sendo desenhados depois da translação. Segue um exemplo:

        glTranslatef(0, 0, -10);

        glBegin(GL_QUADS);
        glColor3f(1,0,0);
        glVertex3f(-1, 1, 0);
        glVertex3f( 1, 1, 0);
        glVertex3f( 1,-1, 0);
        glVertex3f(-1,-1, 0);
        glEnd();

        glTranslatef(-3, 0, 0);

        glBegin(GL_QUADS);
        glColor3f(0,0,1);
        glVertex3f(-1, 1, 0);
        glVertex3f( 1, 1, 0);
        glVertex3f( 1,-1, 0);
        glVertex3f(-1,-1, 0);
        glEnd();

Em primeiro lugar nós transladamos o sistema de coordenadas inteiro em 10 unidades (-10 sobre o eixo Z), depois desenhamos um quadrado vermelho. Em seguida transladamos o sistema de coordenadas 3 unidades para a esquerda (-3 sobre o eixo X), e desenhamos um quadrado azul. Você deve notar duas coisas: primeiro, não precisamos transladar o quadrado azul de volta em 10 unidades (lembre que o OpenGl é uma máquina de estados, apenas altera o que dizemos para alterar) por já termos configurado o sistema de coordenadas. Em segundo lugar, quando transladamos o sistema de coordenados para a esquerda antes de desenhar o quadrado azul, isso não afeta o quadrado vermelho. Lembre, se já tiver sido desenhado antes da translação, então a translação não afetará ele.

Agora iremos passar para outra coisa legal, rotação. A essa altura, você já deve saber o que rotação é, de forma que não iremos nos aprofundar nesses conceitos. Iremos observar que a rotação depende do eixo. Você pode rotacionar algo pelo eixo X, pelo eixo Y ou pelo eixo Z.

Quando você estiver rotacionando algo no eixo X, imagine o objeto sendo cortado por um ponto da esquerda para a direita, ele deve rotacionar em torno desse ponto.

Quando estiver rotacionando em torno do eixo Y, o ponto vai de baixo para cima, imagine o objeto sendo girado nesse ponto.

E, naturalmente, rotacionar em torno do eixo Z faz o ponto ir de trás para a frente.

Abaixo segue um exemplo:

        glTranslatef(0, 0, -10);

        glRotatef(45, 0, 0, 1);

        glBegin(GL_QUADS);
        glColor3f(1,0,0);
        glVertex3f(-1, 1, 0);
        glVertex3f( 1, 1, 0);
        glVertex3f( 1,-1, 0);
        glVertex3f(-1,-1, 0);
        glEnd();

Em primeiro lugar, empurramos o sistema de coordenadas para trás para podermos ver, depois rotacionamos ele 45 graus sobre o eixo Z. O uso de glRotate(45,1,0,0) rotaciona 45 graus no eixo X. Além disso, glRotate(45,1,0,1) rotaciona ambos os eixo X e Z (conhecido como uma rotação arbitrária).

Agora, para finalizar esse tutorial, iremos criar nossa primeira animação, uma quadrado giratório. Iremos fazer isso pela criação de uma variável de ponto flutuante para guardar o grau de rotação, e então iremos incrementando essa variável a cada frame resultando em um quadrado giratório.

#include <stdlib.h>
#include <GL/gl.h>
#include <GL/glu.h>
#include <SDL/SDL.h>

using namespace std;

// Width & Height of window
#define width 640
#define height 480

// Variable storing degree of rotation
float Angle = 0.0;

// Kill program
void endProgram(int code) {SDL_Quit();    exit(code);}

// Handle SDL keypresses
void handleKeys(SDL_keysym* keysym, bool state) {
    switch(keysym->sym) {
        case SDLK_ESCAPE:    endProgram(0); break;
    }
}

// Process SDL events
void processEvents() {
    SDL_Event event;
    while(SDL_PollEvent(&event)) {
        switch(event.type) {
            case SDL_KEYDOWN:    handleKeys(&event.key.keysym, true ); break;
            case SDL_KEYUP  : handleKeys(&event.key.keysym, false);    break;
            case SDL_QUIT   : endProgram(0); break;
        }
    }
}

void mainLoop() {
    while(true) {
        processEvents();

        glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); // Clear color and depth buffer
        glLoadIdentity(); // Reset orientation

        glTranslatef(0, 0, -10);

        glRotatef(Angle, 0, 0, 1);

        glBegin(GL_QUADS);
        glColor3f(1,0,0);
        glColor3f(1,0,0); glVertex3f(-1, 1, 0);
        glColor3f(1,1,0); glVertex3f( 1, 1, 0);
        glColor3f(0,1,0); glVertex3f( 1,-1, 0);
        glColor3f(0,0,1); glVertex3f(-1,-1, 0);
        glEnd();

        SDL_GL_SwapBuffers(); // Update screen

        // Increase degree of rotation to spin QUAD
        Angle+=.25;

    }
}

// Setup OpenGL perspective
void setupOpengl() {
    glViewport(0, 0, width, height);
    glMatrixMode(GL_PROJECTION);
    glEnable(GL_DEPTH_TEST);
    gluPerspective(45, (float)width/height, .1, 100);
    glMatrixMode(GL_MODELVIEW);
}

// Init everything
int main(int argc, char* argv[]) {
    SDL_Init(SDL_INIT_VIDEO);
    SDL_SetVideoMode(width, height, 24, SDL_OPENGL | SDL_GL_DOUBLEBUFFER);
    setupOpengl();
    mainLoop();
    return 0;
}

Clique aqui para baixar o código fonte -> ogl_part1

Para compilar e executar:

 g++ -o ogl_part1 ogl_part1.cpp -lSDL -lGLU -lGLU
./ogl_part1