OpenGL – Básico (C++)

En este post se veremos el código básico en C++ para dibujar un triangulo en OpenGL

Necesitamos, primeramente la librería GLFW para la ventana en donde dibujaremos nuestro triangulo. Luego haremos uso GLEW para escribir las instrucciones con la cual se dibujará el triangulo.

En este ejemplo básico no se verá el uso de un Shader (fragment y vertex, que básicamente es un programa como cualquier otro que se ejecuta en la GPU).

Para un triangulo necesitamos 3 vértices (o puntos) obviamente no colineales, nuestro vértices en este ejemplo serán:

V1 = { -0.5, -0.5, 0.0 }
V2 = { 0.5, -0.5, 0.0 }
V3 = { 0.0, 0.5, 0.0 }

Nuestro triangulo tendrá por lo tanto la siguiente forma:

En C++ podemos representar nuestro vértices por una matriz:

float vertices [] = {
    -0.5f, -0.5f, 0.0f,
     0.5f, -0.5f, 0.0f,
     0.0f,  0.5f, 0.0f
};

Con eso tenemos nuestros vértices en nuestro programa de C++ almacenado en ‘vértices’ pero OpenGL no puede dibujar los vértices porque dicha matriz ‘vértices’ no es visible para él, ya que OpenGL trabaja en nuestra GPU, entonces ¿Cómo la hacemos visible la información de ‘vértices’ para OpenGL? Tenemos que enviar toda la información de ‘vértices’ a la memoria de nuestra GPU y esto se logra con los VBO (VertexBufferObject).

Los vertex buffer object (VBO) nos sirven para almacenar datos que luego pueda usar OpenGL, pero los VBO funcionan dentro de algo llamado Vertex Array Object (VAO) que sirven para almacenar el estado de lo que queremos dibujar en pantalla, por estado nos referimos a por ejemplo los buffers que contienen los datos a dibujar en cierto VAO, si por el momento no entiende bien lo que es un VAO más adelante se explicará mejor.

VBO (VertexBufferObject) para nuestro triangulo

Debemos tener en cuenta que los buffer object son objetos que se crean en el GPU, no en nuestro programa de C++ por lo que no los podemos manipular directamente desde C++, sin embargo podemos acceder a nuestros BufferObject que están en el GPU desde C++ conociendo su identificador. Entonces es lógico que cuando creamos un BufferObject debemos guardar en alguna variable su identificador para así poderlo llamar desde C++, de lo contrario nuestro BufferObject quedaría «perdido a la deriva» (un decir).
Crearemos nuestro VBO así:

unsigned int vbo;
glGenBuffers(1, &vbo);

Los identificadores de los VBO son números, más específicamente de tipo entero sin signo (unsigned int en C++), entonces creamos una variable de ese tipo en C++. Luego la función glGenBuffers() nos crea el buffer que queremos dentro de la GPU, el primer parámetro que es 1 en este caso es para la cantidad de objetos buffer que quieres que cree dicha función y en nuestro caso 1 es suficiente, el segundo parámetro es para que guarde en dicha variable el número identificador de dicho buffer que se creó, el cual pasamos por referencia para que cambie la variable original ya que si solo ponemos ‘vbo’ en lugar de ‘&vbo’ la variable vbo no se modificaría.

Array de C++ hacia Buffer de OpenGL:
Ahora ya tenemos nuestro Buffer en la GPU (vacío), un buffer es como una pequeña memoria dentro del GPU en la cual nosotros podemos guardar información, entonces ahora debemos pasar la información de nuestro array de C++ ‘vertices’ a nuestro buffer creado, para esto usamos la funcion glBufferData().

Antes debemos tener en cuenta que la forma en que OpenGL trabaja es ‘seleccionando‘ y ‘deseleccionando‘ cosas, es decir que para modificar algún objeto de OpenGL primero debemos ‘seleccionarlo‘ y luego lo que hagamos después de ‘seleccionar’ dicho objeto afectará a dicho objeto. Las funciones para ‘seleccionar‘ un objeto de OpenGL por lo general tienen en su nombre la palabra ‘bind’.

Siguiendo con nuestro proceso para enviar la información a nuestro buffer, ya sabemos que debemos hacerlo con glBufferData() pero antes de llamar a glBufferData() debemos seleccionar nuestro buffer para que así la función glBufferData() modifique a dicho buffer.
Este es el código para esto:

glBindBuffer(GL_ARRAY_BUFFER, vbo); // "selecciona" buffer
glBufferData(GL_ARRAY_BUFFER, 9*sizeof(float), vertices, GL_STATIC_DRAW );

glBindBuffer()
En la primera línea seleccionamos nuestro buffer pasando el identificador de dicho buffer que deseamos seleccionar (el cual se encuentra en la variable ‘vbo’) como segundo argumento, el primer argumento es nuestro ‘objetivo’ es decir seleccionamos nuestro buffer pero ¿en que ‘modo’?, un buffer object tiene muchas maneras de ser seleccionado como se puede ver en su documentación (https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glBindBuffer.xhtml), este tutorial es básico por lo que es suficiente decir que el argumento GL_ARRAY_BUFFER simplemente indica que el buffer se seleccione como un array, ya que nuestra data ‘vertices’ es una array por lo que es lógico que cuando lo mandemos a nuestro buffer este sea un array también o que funcione como uno.

glBufferData()
Esta función es para enviar la data desde C++ al buffer que seleccionamos en la línea anterior. El primer argumento es nuevamente GL_ARRAY_BUFFER.
El segundo argumento es la cantidad en bytes que queremos mandar al buffer en este caso queremos pasas una matriz de 9 floats (3 por c/ uno de los 3 vertices), la función sizeof(float) nos retorna el tamaño de un float, lo multiplicamos por 9 y obtenemos el total.
El tercer argumento es la matriz en C++ de quien queremos enviar los datos, en este caso ‘vertices’.
El cuarto argumento GL_STATIC_DRAW es un poco más técnico de explicar, la otra opción es GL_DYNAMIC_DRAW y la diferencia podriamos decir que uno es modificable en tiempo de ejecución (GL_DYNAMIC_DRAW) y GL_STATIC_DRAW no, como quiza podrian ser los arrays dinamicos y arrays estáticos en C++, en la documentación puede encontrar información más clara (https://www.khronos.org/registry/OpenGL-Refpages/es1.1/xhtml/glBufferData.xml)

Con esto ya tenemos la data de ‘vértices’ en nuestro buffer de OpenGL (que esta en la GPU) pero ahora nos surge un nuevo problema: La data está en nuestro buffer sin embargo necesitamos decirle a OpenGL lo que significa esa data, para que luego OpenGL pueda usar esa data para dibujar algo.

Para esto haremos uso de los VAO (Vertex Array Object), estos objetos en OpenGL son objetos que existen dentro de la GPU, y nos sirven para almacenar la información sobre cómo dibujar nuestro objeto, hasta ahora tenemos nuestros vértices en nuestro buffer sin embargo necesitamos un VAO para que OpenGL pueda saber como extraer la data del buffer (en este caso los vértices de nuestro triangulo) y poder dibujar dicha figura.

La creación de un VAO es similar a un VBO y trabajar con el es similar a trabajar con cualquier objeto en OpenGL, al crearlo necesitamos una variable para guardar el identificador de dicho objeto VAO, luego necesitamos ‘seleccionar’ dicho VAO que creamos para poder usarlo y poder modificarlo, veámoslo más claro en las siguientes líneas de código:

unsigned int vao; // variable para guardar el id de nuestro VAO
glGenVertexArrays(1, &vao); // crea el VAO 

En la primera línea creamos un variable de tipo entero sin signo (unsigned int) para almacenar dentro el identificador de nuestro VAO que crearemos, en la segunda línea llamamos a la función para crear el VAO, esta función crea en nuestra GPU el VAO y el identificador de dicho VAO lo almacena en la variable ‘vao’ la cual se pasa por referencia por lo explicado anteriormente.

Una vez tenemos creado nuestro VAO necesitamos guardar dentro de él lo necesario para que OpenGL pueda dibujar nuestro triangulo, esto lo logramos con las funciones glVertexAttribPointer() y glEnableVertexAttribArray(), estas funciones trabajan sobre el VAO que se encuentre ‘seleccionado’, asi que primero debemos seleccionar nuestro VAO creado, esto lo hacemos con la instrucción glBindVertexArray pasándole como argumento el identificador del VAO que queremos seleccionar, en este caso ‘vao’, veamoslo en codigo:

glBindVertexArray(vao); // seleccionamos nuestro VAO
glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, (void *) 0);
glEnableVertexAttribArray(0);

glBindVertexArray()
En la primera linea seleccionamos nuestro VAO creado anteriormente para que las siguientes funciones afecten a dicho VAO.

glVertexAttribPointer()
Esta función la llamamos en la segunda línea y nos sirve para que nuestro VAO sepa como es que debe extraer la data del buffer para cada vértice para que así pueda dibujarlo.
Debemos tener en claro que esta función lo que hará es extraer la información del buffer que se encuentre seleccionado e indicado como GL_ARRAY_BUFFER, por lo que es indispensable antes de llamar a esta función primero haber seleccionado el buffer (con glBindBuffer(GL_ARRAY_BUFFER, vbo) ) que contiene la información, y obviamente también tener seleccionado el VAO.
El primer parámetro es un índice de atributo, por el momento no se preocupe en entender lo que es un índice de atributo, esto es algo que se entenderá mucho mejor cuando expliquemos los Shader en otro post posterior, por el momento solo debe saber que el índice de atributo numeró cero corresponde a una variable la cual es para almacenar la posición de nuestro vértices es por ello que la usamos, ya que los vértices que se lean del buffer serán las coordenadas o posiciones de nuestros vértices.
El segundo parámetro indica cuantas posiciones o vértices se leerán del buffer en nuestro caso 3.
El tercer parámetro indica el tipo de dato con el cual están representados nuestros datos en nuestro caso es float el cual indicamos por GL_FLOAT
El cuarto parámetro es para normalización, en nuestro caso no es importante conocer el significado de esto simplemente indicamos falso.
El quinto parámetro es importante cuando tenemos más información que solamente vértices en el buffer, en este caso todo el buffer solo contiene nuestros vértices por lo que solo ponemos 0, no se preocupe por entender este parámetro por ahora, en un post posterior se podrá entender mejor para que es.
El sexto parámetro indica el inicio desde donde se debe leer el buffer, en nuestro caso desde la posición inicial, lo peculiar en este argumento es que no simplemente podemos pasar un numero sino que necesitamos convertir dicho número a puntero, es por ello que colocamos (void *) 0.

glEnableVertexAttribArray()
Esta función simplemente lo que hace es habilitar el atributo de vértice, el argumento es cero ya que estamos habilitando el mismo atributo de vértice que indicamos en la función anterior el cual es para las posiciones de nuestro triangulo. Es bueno saber que esta funcion la hemos llamado antes de glVertexAttribPointer(), sin embargo no es necesario ese orden, simplemente se debe llamar cuando nuestro VAO este seleccionado, es decir en una línea posterior (en el flujo de ejecución) a la llamada a glBindVertexArray().

Una vez hecho todo esto nuestro triangulo está listo para ser dibujado, simplemente toca llamar a la función:

glDrawArrays()
Esta función dibuja la información contenida en el VAO seleccionado, por lo que es necesario antes de llamar a esta función seleccionar el VAO que contiene el estado del objeto que queremos dibujar. La llamada a esta función es la siguiente:

glDrawArrays(GL_TRIANGLES, 0, 3);

El primer parámetro es para indicar la primitiva que queremos usar en este caso triangulo por lo que es GL_TRIANGLE.
El segundo parámetro indica la posición inicial de donde leer, en nuestro caso cero o sea el inicio.
El tercer parámetro indica cuantos vértices necesitamos leer para dibujar, en nuestro caso es 3.

El código completo a continuación:

/*
 * Draw a basic triangle in OpenGL
 */

#include <GL\glew.h>
#include <GLFW\glfw3.h>

#include <iostream>

using namespace std;

// variables globales
GLFWwindow *window;
unsigned int vao, vbo;

float vertices [] = {
    -0.5f, -0.5f, 0.0f,
     0.5f, -0.5f, 0.0f,
     0.0f,  0.5f, 0.0f
};

inline const void setup_win_n_libs() {
    if (!glfwInit()) exit(EXIT_FAILURE);
    window = glfwCreateWindow(400, 400, "triangle", NULL, NULL);
    glfwMakeContextCurrent(window);
    if (glewInit() != GLEW_OK) exit(EXIT_FAILURE);
    cout<<"GL_VERSION: "<<glGetString(GL_VERSION)<<endl;
    glfwSwapInterval(1);
}

inline void setup_my_object_data() {

    glGenBuffers(1, &vbo);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, 9*sizeof(float), vertices, GL_STATIC_DRAW );
    
    glGenVertexArrays(1, &vao);
    glBindVertexArray(vao);
    glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, (void *) 0);
    glEnableVertexAttribArray(0);
}

void draw() {
    glBindVertexArray(vao);
    glDrawArrays(GL_TRIANGLES, 0, 3);
}

void inline clean() {
    glDeleteBuffers(1, &vbo);
    glDeleteVertexArrays(1, &vao);
    glfwDestroyWindow(window);
    glfwTerminate();
}

int main() {
    setup_win_n_libs();
    setup_my_object_data();

    while(!glfwWindowShouldClose(window)) {
        draw();
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    clean();
    exit(EXIT_SUCCESS);
    return 0;
}

OUTPUT:

En este post explicamos lo relacionado a OpenGL más específicamente las funciones de GLEW, no nos enfocamos mucho en las funciones de GLFW que son para nuestra ventana en la cual dibujamos nuestro objeto, si desea saber más sobre las funciones de GLFW puede revisar https://www.glfw.org/docs/latest/quick.html, donde se encuentran explicadas de forma muy clara cada una de las funciones.


Advertencia: Este post puede contener errores, si encuentra uno puede colocarlo en los comentarios para contribuir a mejorar esta web.
Puede escribir al autor en rogrp6@gmail.com

¡Gracias por visitar este post!

Enlaces externos:

https://www.khronos.org/opengl/wiki/Vertex_Specification
https://www.glfw.org/docs/latest/quick.html
http://docs.gl/

Un comentario sobre “OpenGL – Básico (C++)

Deja un comentario