Renderizar un Triángulo usando OpenGL (usando Shaders)

En este artículo veremos cómo renderizar un triángulo usando OpenGL. Un triángulo es probablemente la forma más simple que puedes dibujar en OpenGL después de puntos y líneas y cualquier geometría complicada que hagas estará compuesta por varios triángulos unidos.
Usaremos la tubería programable, por lo que también escribiremos programas simples de sombreado y los compilaremos y los usaremos más tarde para renderizar. Ahora, esto hará que nuestro programa sea un poco largo, pero lo bueno es que tenemos que hacer esto solo una vez y luego reutilizar el código que ya hemos escrito. Esto es realmente cierto para la mayoría del código OpenGL y, por lo tanto, tendemos a escribir la mayoría de los códigos OpenGL en diferentes funciones y reutilizarlos varias veces más tarde.
Las bibliotecas que usaremos para esto son Glew y Glut y Ç++ como nuestro lenguaje de programación.
Configuración del entorno 
 

  1. Descargue los últimos archivos de encabezado, biblioteca y dll de gew y glut.
  2. En Visual Studio, cree un proyecto de aplicación de consola Win 32.
  3. Vaya a las propiedades de su proyecto.
  4. Importe e incluya encabezado y biblioteca para toda la configuración.
  5. Coloque los archivos dll donde están sus archivos fuente.

El programa completo (la explicación sigue después del programa) 
 

CPP

// CPP program to render a triangle using Shaders
#include <GL\freeglut.h>
#include <GL\glew.h>
#include <iostream>
#include <string>
 
std::string vertexShader = "#version 430\n"
                           "in vec3 pos;"
                           "void main() {"
                           "gl_Position = vec4(pos, 1);"
                           "}";
 
std::string fragmentShader = "#version 430\n"
                             "void main() {"
                             "gl_FragColor = vec4(1, 0, 0, 1);"
                             "}";
 
// Compile and create shader object and returns its id
GLuint compileShaders(std::string shader, GLenum type)
{
 
    const char* shaderCode = shader.c_str();
    GLuint shaderId = glCreateShader(type);
 
    if (shaderId == 0) { // Error: Cannot create shader object
        std::cout << "Error creating shaders";
        return 0;
    }
 
    // Attach source code to this object
    glShaderSource(shaderId, 1, &shaderCode, NULL);
    glCompileShader(shaderId); // compile the shader object
 
    GLint compileStatus;
 
    // check for compilation status
    glGetShaderiv(shaderId, GL_COMPILE_STATUS, &compileStatus);
 
    if (!compileStatus) { // If compilation was not successful
        int length;
        glGetShaderiv(shaderId, GL_INFO_LOG_LENGTH, &length);
        char* cMessage = new char[length];
 
        // Get additional information
        glGetShaderInfoLog(shaderId, length, &length, cMessage);
        std::cout << "Cannot Compile Shader: " << cMessage;
        delete[] cMessage;
        glDeleteShader(shaderId);
        return 0;
    }
 
    return shaderId;
}
 
// Creates a program containing vertex and fragment shader
// links it and returns its ID
GLuint linkProgram(GLuint vertexShaderId, GLuint fragmentShaderId)
{
    GLuint programId = glCreateProgram(); // create a program
 
    if (programId == 0) {
        std::cout << "Error Creating Shader Program";
        return 0;
    }
 
    // Attach both the shaders to it
    glAttachShader(programId, vertexShaderId);
    glAttachShader(programId, fragmentShaderId);
 
    // Create executable of this program
    glLinkProgram(programId);
 
    GLint linkStatus;
 
    // Get the link status for this program
    glGetProgramiv(programId, GL_LINK_STATUS, &linkStatus);
 
    if (!linkStatus) { // If the linking failed
        std::cout << "Error Linking program";
        glDetachShader(programId, vertexShaderId);
        glDetachShader(programId, fragmentShaderId);
        glDeleteProgram(programId);
 
        return 0;
    }
 
    return programId;
}
 
// Load data in VBO and return the vbo's id
GLuint loadDataInBuffers()
{
    GLfloat vertices[] = { // vertex coordinates
                           -0.7, -0.7, 0,
                           0.7, -0.7, 0,
                           0, 0.7, 0
    };
 
    GLuint vboId;
 
    // allocate buffer space and pass data to it
    glGenBuffers(1, &vboId);
    glBindBuffer(GL_ARRAY_BUFFER, vboId);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
 
    // unbind the active buffer
    glBindBuffer(GL_ARRAY_BUFFER, 0);
 
    return vboId;
}
 
// Initialize and put everything together
void init()
{
    // clear the framebuffer each frame with black color
    glClearColor(0, 0, 0, 0);
 
    GLuint vboId = loadDataInBuffers();
 
    GLuint vShaderId = compileShaders(vertexShader, GL_VERTEX_SHADER);
    GLuint fShaderId = compileShaders(fragmentShader, GL_FRAGMENT_SHADER);
 
    GLuint programId = linkProgram(vShaderId, fShaderId);
 
    // Get the 'pos' variable location inside this program
    GLuint posAttributePosition = glGetAttribLocation(programId, "pos");
 
    GLuint vaoId;
    glGenVertexArrays(1, &vaoId); // Generate VAO
 
    // Bind it so that rest of vao operations affect this vao
    glBindVertexArray(vaoId);
 
    // buffer from which 'pos' will receive its data and the format of that data
    glBindBuffer(GL_ARRAY_BUFFER, vboId);
    glVertexAttribPointer(posAttributePosition, 3, GL_FLOAT, false, 0, 0);
 
    // Enable this attribute array linked to 'pos'
    glEnableVertexAttribArray(posAttributePosition);
 
    // Use this program for rendering.
    glUseProgram(programId);
}
 
// Function that does the drawing
// glut calls this function whenever it needs to redraw
void display()
{
    // clear the color buffer before each drawing
    glClear(GL_COLOR_BUFFER_BIT);
 
    // draw triangles starting from index 0 and
    // using 3 indices
    glDrawArrays(GL_TRIANGLES, 0, 3);
 
    // swap the buffers and hence show the buffers
    // content to the screen
    glutSwapBuffers();
}
 
// main function
// sets up window to which we'll draw
int main(int argc, char** argv)
{
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE);
    glutInitWindowSize(500, 500);
    glutInitWindowPosition(100, 50);
    glutCreateWindow("Triangle Using OpenGL");
    glewInit();
    init();
    glutDisplayFunc(display);
    glutMainLoop();
    return 0;
}

Salida 
Ahora, cuando ejecute el programa, debería tener un triángulo rojo en la pantalla que debería parecerse a la imagen de abajo.
 

Función main() La función 
principal configura nuestra ventana. Inicializamos glut, glew, especificamos el tamaño de las ventanas, su posición en la pantalla, especificamos el modo de visualización (que solo indica qué espacio de color utilizará la ventana y si usará un búfer simple o doble), establecemos el título de las ventanas, establecemos el método de devolución de llamada que glut usará para dibujar cosas en esta ventana y finalmente hacer que la ventana sea visible.
Función loadDataInBuffers() 
Un triángulo se compone de tres puntos de coordenadas. Estos puntos (almacenados como array de punto flotante) deben enviarse a la memoria del procesador de gráficos. 
La forma en que nos referimos a una ubicación en la memoria del sistema es relativamente fácil, solo obtenga un puntero a la ubicación de la memoria, pero usar la memoria de su GPU no es tan sencillo. 
Hacemos esto por algo conocido como objetos de búfer .
Los objetos de búfer son la forma en que carga sus datos en la V-RAM y los consulta más tarde. 
Por lo general, hay tres pasos que debe seguir para obtener sus datos en la memoria de video. 
 

  1. Genere un identificador entero sin signo para su objeto de búfer.
  2. Vincule ese objeto de búfer a un objetivo.
  3. Especifique el tamaño del búfer y, opcionalmente, cargue datos en él.

Recuerde que OpenGL espera coordenadas de vértice en el rango de [-1, 1]. Cualquier cosa fuera de este rango se recorta. Sin embargo, somos libres de elegir nuestro propio sistema de coordenadas; de hecho, es una práctica muy común definir nuestro propio sistema de coordenadas y definir nuestros objetos de acuerdo con este sistema de coordenadas y luego cambiarlo al sistema de coordenadas OpengGL.
Pero en esta publicación viajaremos por el camino más fácil y especificaremos las coordenadas de nuestro triángulo en el rango [-1, 1]. Las coordenadas FYI en este rango se conocen como coordenadas de dispositivo normalizadas (NDC).
Shaders 
A lo largo de la canalización de renderizado, los datos de entrada (aquí, las coordenadas de los vértices) pasan por varias etapas. Controlamos estas etapas usando algo llamado Shaders 
Los sombreadores son programas que se ejecutan en GPU. Los sombreadores en OpenGL están escritos en un lenguaje especial comúnmente conocido como GLSL (lenguaje de sombreado OpenGL) que notará que es muy similar a C y C++. Los sombreadores le brindan control directo sobre la canalización de gráficos. GLSL se incluyó formalmente en el núcleo de OpenGL 2.0 en 2004 por OpenGL ARB.
Los dos shaders que necesitarás todo el tiempo son
 

  1. Vertex Shader: estos shaders se ejecutan una vez por cada vértice (en el caso de un triángulo se ejecutará 3 veces) que procesa la GPU. Entonces, si su escena consta de un millón de vértices, el sombreador de vértices se ejecutará un millón de veces una vez para cada vértice. El trabajo principal de un sombreador de vértices es calcular las posiciones finales de los vértices en la escena. Lo hace escribiendo un vector (este vector tiene 4 componentes) en una variable de salida especial conocida como gl_Position (solo disponible en vertex shader). La entrada al sombreador de vértices se especifica usando un calificador de tipo in y la salida con un calificador out antes de la variable. La variable de entrada y salida debe tener alcance global.
  2. Fragment Shader: después de que su geometría haya pasado por otras etapas intermedias, si finalmente llega al rasterizador. Rasterizer divide su geometría en fragmentos. Luego, el sombreador de fragmentos se ejecuta para cada fragmento en su geometría (o triángulo). El trabajo del sombreador de fragmentos es determinar el color final de cada fragmento. Escribe el color final en una variable de salida especial gl_FragColor (solo disponible para fragmentar el sombreador). El valor pasado a gl_FragColor es un vector que contiene valores rgba para ese fragmento.

El código de sombreado se almacena como una variable global de strings. Vincularemos esta variable a los datos en nuestro programa openGL más adelante. La posición del vértice se pasa a gl_Position sin modificación.
Nota: Es una práctica común colocar sus códigos de sombreado en diferentes archivos y luego almacenar el contenido de los archivos en variables de string.
Una vez que haya terminado de escribir su sombreador, debe crear objetos de sombreado, adjuntar su código fuente correspondiente, compilarlos y verificar si hay errores.
Función compileShaders()
Esta función compila el código del sombreador, comprueba si hay errores, crea un objeto del sombreador y devuelve su id. 
Llamaremos a esta función una vez para vertex y fragment shader
linkProgram() Function 
Los sombreadores que escriba para renderizar deben agruparse en un programa de sombreado. 
Esta función agrupa estos objetos de sombreado en un programa de sombreado, los vincula (que crea un archivo ejecutable para ese programa) y verifica los errores que pueden ocurrir en este proceso.
Función init()
Utiliza todas las funciones anteriores y pone todo junto. 
Usamos lo que se conoce como VAO (Vertex Array Objects)
VAO:Un objeto de array de vértices (VAO) es un objeto OpenGL que almacena todo el estado necesario para proporcionar datos de vértice. Almacena el formato de los datos de vértice, así como los objetos de búfer que proporcionan las arrays de datos de vértice. Tenga en cuenta que los VAO no copian, congelan ni almacenan el contenido de los búferes a los que se hace referencia; si cambia cualquiera de los datos en los búferes a los que hace referencia un VAO existente, los usuarios del VAO verán esos cambios.
Un programa de sombreado asigna a cada una de las variables de entrada del sombreador de vértices (también conocido como atributos de vértice) y variables uniformes (variables cuyos valores no cambian para la primitiva que estamos representando) una posición que necesitamos saber si queremos vincularlos a su fuente de datos.
función mostrar() 
Recuerde que aún no le hemos dicho a GPU que comience a renderizar. La función de visualización es lo que le da este comando a la GPU. 
Glut llama a esta función de devolución de llamada cada vez que necesita dibujar cosas en la pantalla.
 

Publicación traducida automáticamente

Artículo escrito por Jitendra Tiwari y traducido por Barcelona Geeks. The original can be accessed here. Licence: CCBY-SA

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *