domingo, 21 de julio de 2013

I - Dibujando un Triángulo

Lo primero que vamos a hacer es dibujar un triángulo con un vértice de cada color y un degradado según se aleja de los vértices. Si alguno habéis visto algún ejemplo de OpenGL, éste es el típico.
El ejemplo esta basado en el libro oficial de programación en OpenGL que ya he comentado en varios post, lo he simplificado y pasado a la estructura de juegos.

Lo primero como siempre el ".h".
Por un lado tenemos lo mismo de siempre: la ventana, el contexto, la variable running, las constantes de configuracion y las 5 funciones del gameLoop con sus correspondientes OnXXXXXX, y el constructor.
Por otro lado tenemos lo nuevo: 
  • Dos funciones, una para configurar OpenGL y otra para inicializar los datos que vamos a utilizar en el render.
  • Una constante que nos dice cuantos vértices se van a dibujar, como es un triángulo: 3
  • Dos arrays de una casilla que ya veremos para que sirven. GLuint, es el tipo genérico (cross-platform) de unsigned int, para evitar tener problemas.
  • Attrib_Locs es simplemente una manera cómoda de asignar dos constantes más y evitar que se solapen los valores.
El Demo_OGL_1.h:
#ifndef DEMO_OGL_I_H_
#define DEMO_OGL_I_H_
#include <iostream>
#include <GL/gl3w.h>
#include <GL/gl.h>
#include "SDL2/SDL.h"
#include "LoadShaders.h"

class Demo_OGL_1 {
private:
    bool running;
    SDL_Window* window;
    SDL_GLContext ctxt;

    static const uint32_t   WIN_HEIGHT = 512; //px
    static const uint32_t   WIN_WIDTH  = 512; //px
    static const char*      WIN_TITLE; //px

    /***************************************************/

    enum Attrib_Locs { vPosition = 0, vColor = 1 };

    GLuint  vao[1];
    GLuint  buffer[1];

    static const GLuint  NumVertices = 3;
public:

    Demo_OGL_1();
    /*
     * GAME LOOP FUNCTIONS
     */

    int Execute(){ return OnExecute(); }
    bool Init(){ return OnInit(); }
    void Loop(){ return OnLoop(); }
    void Render(){ return OnRender(); }
    void Cleanup(){ return OnCleanup(); }
    void Event(SDL_Event* Event){ OnEvent(Event); }


    int OnExecute();
    bool OnInit();
    void OnEvent(SDL_Event* Event);
    void OnLoop();
    void OnRender();
    void OnCleanup();

    /***************************************************/

    void SetupOpenGL();
    void InitData();
};


#endif /* DEMO_OGL_I_H_ */
Y ahora ponemos el Demo_OGL_1.cpp:
#ifndef DEMO_OGL_I_H_

#include "Demo_OGL_1.h"
#include <GL/gl3w.h>
#include <GL/gl.h>
#include "LoadShaders.h"

const char* Demo_OGL_1::WIN_TITLE = "Titulo de la Ventana";

Demo_OGL_1::Demo_OGL_1() : running(false), window(NULL), ctxt(NULL){}

void Demo_OGL_1::OnEvent(SDL_Event* event) {
    switch (event->type) {
        case SDL_KEYUP:
            switch(event->key.keysym.sym){
                case SDLK_v:
                    std::cout << glGetString(GL_VERSION) << std::endl;
                    break;
                case SDLK_ESCAPE:
                    running = false;
                    break;
                default:
                    break;
            }
            break;
        case SDL_QUIT:
            running = false;
            break;
        default:
            break;
    }
}
void Demo_OGL_1::OnLoop() {}

void Demo_OGL_1::OnCleanup() {
    glUseProgram(0);
    glDeleteVertexArrays(1, vao);
    glDeleteBuffers(1, buffer);
    SDL_GL_DeleteContext(ctxt);
    SDL_DestroyWindow(window);
    SDL_Quit();
}


int Demo_OGL_1::OnExecute() {
    if (!Init())
        return -1;

    SDL_Event event;

    while (running) {

        while (SDL_PollEvent(&event))
            Event(&event);

        Loop();
        Render();
    }

    Cleanup();

    return 0;
}

void Demo_OGL_1::OnRender() {

    glClear( GL_COLOR_BUFFER_BIT );

    glBindVertexArray( vao[0] );
    glDrawArrays( GL_TRIANGLES, 0, NumVertices );

    SDL_GL_SwapWindow(window);
}

bool Demo_OGL_1::OnInit() {
    if(SDL_Init(SDL_INIT_EVERYTHING) < 0)
        return false;

    window = SDL_CreateWindow(WIN_TITLE,
            SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
            WIN_WIDTH, WIN_HEIGHT,
            SDL_WINDOW_SHOWN | SDL_WINDOW_OPENGL);
    if(!window)
        return false;

    SetupOpenGL();
    InitData();

    running = true;

    return true;
}
void Demo_OGL_1::SetupOpenGL() {

   SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
   SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 32);
   ctxt = SDL_GL_CreateContext(window);

   //vsync ON
   SDL_GL_SetSwapInterval(1);

   if (gl3wInit()) {
       std::cout << "Error al Inicializar GL3W" << std::endl;
   }

   glClearColor(0.0, 0.0, 0.0, 1.0);

}
void Demo_OGL_1::InitData() {

    glGenVertexArrays( 1, vao );
    glBindVertexArray( vao[0] );

    struct VertexData {
        GLfloat color[3];
        GLfloat position[4];
    };

    VertexData vertices[NumVertices] = {
        {{  1.00, 0.00, 0.00 }, {  0.00, 0.90 }},
        {{  0.00, 1.00, 0.00 }, {  0.90, -0.90 }},
        {{  0.00, 0.00, 1.00 }, { -0.90, -0.90 }}

    };


    glGenBuffers( 1, buffer );
    glBindBuffer( GL_ARRAY_BUFFER, buffer[0] );
    glBufferData( GL_ARRAY_BUFFER, sizeof(vertices),
                  vertices, GL_STATIC_DRAW );

    ShaderInfo shaders[] = {
        { GL_VERTEX_SHADER, "Resources/Demo_OGL_I/gouraud.vert" },
        { GL_FRAGMENT_SHADER, "Resources/Demo_OGL_I/gouraud.frag" },
        { GL_NONE, NULL }
    };

    GLuint program = LoadShaders( shaders );
    glUseProgram( program );

    glVertexAttribPointer( vColor, 3, GL_FLOAT,
                           GL_TRUE, sizeof(VertexData), NULL );
    glVertexAttribPointer( vPosition, 2, GL_FLOAT,
                           GL_FALSE, sizeof(VertexData),
                           (const GLvoid*)sizeof(vertices[0].color) );

    glEnableVertexAttribArray( vColor );
    glEnableVertexAttribArray( vPosition );
}

Veamos:
  • La gestión de eventos se ha simplificado para poder salir del programa y mostrar la versión pulsando "v".
  • El OnLoop sigue vacío, no hacemos modificaciones en el modelo porque no hay.
  • OnCleanup, destruimos la información que hemos usado y limpiamos SDL como venimos haciendo hasta ahora.
  • OnExecute se mantiene igual, el Game Loop.
  • OnRender tiene 4 instrucciones:
    • glClear( GL_COLOR_BUFFER_BIT ); Limpia el buffer de color, lo que provoca que todo el contexto se ponga del color que hayamos prefijado. Se prefija en SetupOpenGL a si que ya lo comentaremos.
    • glBindVertexArray( vao[0] ); Volvemos activo el array de vertices para poder utilizarlo. Más tarde nos pararemos en esto.
    • glDrawArrays( GL_TRIANGLES, 0, NumVertices ); Con el array de vértices que hemos vuelto activo antes, dibujamos. GL_TRIANGLES indica que la primitiva que estamos usando son triángulos (cada 3 vértices se construye un triangulo (Ésta aclaración no es por tomaros por tontos, en otro tutorial veremos que otra primitiva nos permite dibujar triángulos pero no cada 3 vértices, sino en todos los vértices a partir del tercero)). Le indicamos el primer vértice (0) y el número de vértices que hay que dibujar.
    • SDL_GL_SwapWindow(window); Aprovechando el doble buffer.
  • OnInit no tiene nada nuevo salvo la llamada a inicializar opengl y la información.
  • SetupOpenGL es simple, activamos el doble buffer, utilizamos 32bit de profundidad, creamos el contexto con SDL, activamos la sincro vertical, inicializamos gl3w y aquí viene el momento de prefijar el color de fondo del contexto OpenGL, es RGBA, por lo tanto: 0 rojo, 0 azul, 0 verde, 1 alpha (negro opaco). Si queréis probar, podeis cambiar los colores con unos valores entre 0.0 y 1.0. Por ejemplo un gris: 0.9 en todo excepto alpha.
  • InitData es la clave de éste ejemplo:
    • glGenVertexArrays( 1, vao );glBindVertexArray( vao[0] );
      No se si soy torpe o no encontré la explicación adecuada, pero me costó entender esto un poco. Voy a intentar explicarlo de la mejor manera que se me ha ocurrido.
      Cuando creamos una variable en cualquier lenguaje de programación, se hace en tres fases:
      • Se nos da un nombre asociado a la variable.
      • Se reserva un hueco en memoria.
      • Inicializamos el hueco que se nos ha dado en memoria.
      Teniendo presente esto, estas dos instrucciones hacen algo parecido a las dos primeras fases de la creación de una variable.
      Le pedimos que genere 1 nombre de array de vertices, y le decimos que nos lo guarde en el
      vao (vertex array objects). Podríamos pedirle más nombres cambiando el 1 por el número que necesitemos, por eso utilizamos un array de objetos y no un puntero a un solo objeto.
      Con
      glBindVertexArray lo que hacemos es la segunda fase: reservamos un hueco de memoria asociado a ese nombre que anteriormente hemos generado. Ademas OpenGL funciona como máquina de estados, ¿Eso que quiere decir? que cuando hacemos el bind, hacemos que ese vertexArray pase a ser el activo, y todas las operaciones que se hagan, se harán (si proceden) sobre él. Por eso en la función OnRender hacemos un bind y después el draw. Primero lo volvemos activo y después lo dibujamos. Creo que se podría omitir en este ejemplo, pero normalmente no vamos a usar un solo vao a si que esta bien irse acostumbrando.
      De esta manera podemos decir que tenemos alojada una variable de tipo vertex array en la memoria de opengl y tenemos un nombre para poder acceder a ella.
    • Las dos siguientes instrucciones son puro C. Creamos una estructura que guarda color y posición de los vértices. Después creamos un array para guardar esa información.
    • glGenBuffers( 1, buffer ); glBindBuffer( GL_ARRAY_BUFFER, buffer[0] );glBufferData( GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW );
      Las dos primeras instrucciones son similares a las de los vao con la diferencia que al hacer el bind le avisamos que es un buffer array. Esto le da una pista a OpenGL acerca de donde debe guardar ese buffer de vértices. Existen mas "pistas" que iremos viendo a lo largo de los tutoriales.
      La última instrucción lo que hace es la tercera de las fases que hemos hablado antes, inicializa el hueco de memoria. Lo parámetros son:
      El hueco de memoria que queremos inicializar.
      El espacio que va a ocupar en bytes.
      El puntero a la estructura o array.
      El tipo de dibujado, en este caso usamos
      static_draw porque no va a cambiar la información del buffer muy amenudo, normalmente quiere decir que no va a cambiar prácticamente nunca en la ejecución del programa. Al igual que el GL_ARRAY_BUFFER, existen varios valores que iremos viendo más adelante.
    • El siguiente array se utiliza para pasárselo al cargador de shaders (que mal suena), simplemente asocia una constante a una ruta en la que se encuentra el shader.
    • Cargamos los shader, nos devuelve el nombre del programa (igual que el los buffers). Este programa ya esta almacenado y compilado en memoria. Una rápida explicación de los shaders: Los shaders son programas (con su main y todo!!) que usa la GPU para el postprocesamiento de las imágenes, para ello hay que darle un código en un lenguaje especifico llamado GLSL (OpenGL Shading Languaje). En tiempo de ejecución este programa se compila y se aloja en la memoria de la GPU.
    • Le decimos a OpenGL que se ponga a usar ese programa para postprocesado.
    • Las siguientes 4 instrucciones las voy a explicar junto con una breve introducción a los shaders, por lo que primero voy a pegar el codigo de los dos shaders. Me vais a perdonar pero el script de coloreado no colorea GLSL, a si que habrá que conformarse con que coloree lo que se parezca a C++:

El vertex shader: gouraud.vert:
#version 400 core

layout( location = 0 ) in vec4 vPosition;
layout( location = 1 ) in vec4 vColor;

out vec4  color;

void
main()
{
    color = vColor;
    gl_Position = vPosition;
}
El fragment shader: gouraud.frag:
#version 400 core

in  vec4 color;

out vec4 fColor;

void
main()
{
    fColor = color;
}
Los shader tienen una sintaxis muy parecida a C. Como por ejemplo: La primera linea donde pone version es una instrucción de preprocesador que le indica como tiene que entender el shader y toda la ejecución empieza por el main.
También existen unas cuantas diferencias que iremos viendo.
  • Hablaremos primero del vertex shader por ser el primero que se procesa.
    El vertex shader actúa vértice a vértice  por lo que tendremos que darle información acerca de los vértices.

    • Después de la versión lo primero que nos encontramos son dos variables. A diferencia de C, aquí las variables "globales" si se recomiendan porque los programas son pequeños y porque es la única manera de establecer valores de entrada y salida del programa.Estas dos variables tienen: 
      • layout( location = n), (n es un número) esto es una manera de localizar las variables fuera del programa.
      • in Se define como variable de entrada de datos. Podremos describir su valor desde nuestro programa C++.
      • vec4 Quiere decir que la variable es de tipo vector de 4 valores, es decir un array de 4 casillas.
      • Por último el nombre de la variable.
      Ahora veamos el código que teniamos escrito en C++:
      glVertexAttribPointer( vColor, 3, GL_FLOAT,
                             GL_TRUE, sizeof(VertexData), NULL );
      glVertexAttribPointer( vPosition, 2, GL_FLOAT,
                             GL_FALSE, sizeof(VertexData),
                             const GLvoid*)sizeof(vertices[0].color) );
      
      vColor y vPosition son dos constantes del enumerado (si os acordais), corresponden con el 0 y el 1 respectivamente.
      La función asocia unos valores del buffer con unas variables del programa.
      El primer valor de la función indica la localización de la variable, en nuestro caso estamos poniendo 0 y 1 para que coincida con el valor de
      location. El nombre de nuestra variable C da igual, lo importante es que el valor es 0 o 1 para poder asociarlo con la location interna.
      Lo siguiente es el número de valores a extraer del buffer y el tipo de valor (float).
      Lo siguiente es la normalización de los datos (si o no), de momento no nos vamos a parar en eso, pero un dato normalizado está entre -1 y 1.
      Los dos últimos valores son el espacio entre valor y valor, y el espacio desde el principio hasta el primer valor.
      Ahora cuando se ejecute el programa, por cada vértice ira secuencialmente seleccionando los valores del buffer para cada uno como se pretende ilustrar en las imágenes.

      Estado del buffer en el vertice primero

      Estado del buffer en el vertice primero

    • Lo siguiente que nos encontramos en el shader es una variable out.
      Las variables
      out sirven para comunicar las diferentes etapas del pipeline de los shaders. Un out, concatena con un in de la siguiente etapa.
    • Por último tenemos el programa en si, asignamos a la variable de salida de color, la variable de entrada directamente.
      Y a la variable que viene por defecto en GLSL (
      gl_Position) la posición en la que se encuentra el vértice.
      Los mas observadores se habrán dado cuenta de que el vector es de 4 y las variables que les pasamos son de 2 y 3 posiciones. Los valores no definidos se predefinen, en el caso de los tres primeros (RGB o XYZ) con 0 y el cuarto (alpha o w) con 1.
  • En el caso del fragment shader no hay mucho que explicar habiendo explicado el vertex antes.
    • Tenemos una variable de entrada color (que es la que tiene de salida el vertex) y una variable de salida color que es el color final que se pintara en pantalla.
    • El cuerpo del programa simplemente pasa el color que recibe. el pipeline hace automáticamente una interpolación de los valores de tal manera que lo que se verá es un degradado.
Por último solo queda explicar las últimas dos lineas de nuestro programa principal: 
glEnableVertexAttribArray( vColor );
glEnableVertexAttribArray( vPosition );

Hace que las dos variables estén habilitadas. Se el indica que ya tiene un valor asociado.

Pues después de tanto leer solo queda probarlo!!

Debería de salir lo siguiente:

Os invito a jugar un poco a cambiar los valores y entender como funciona el código. Una de las pruebas que yo hice fue elevar al cuadrado el valor del color para ver si salia lo que me imaginaba, y si! al estar en unos valores entre 0 y 1, al elevar al cuadrado los valores se hacen mas pequeños excepto en los vértices que se mantiene. Si elevásemos a la enésima potencia tendríamos sólo los vértices coloreados.
Para que os hagais a la idea, elevando a 4 cada componente (fragment shader)


    fColor = vec4(color.x*color.x*color.x*color.x, color.y*color.y*color.y*color.y, color.z*color.z*color.z*color.z, color.w*color.w*color.w*color.w);

tendríamos lo siguiente: 




Espero que os haya gustado.

No hay comentarios:

Publicar un comentario