Kerzan Sobre mí

Introduccion a OpenGL y Graficos para Videojuegos

Esta será una pequeña introducción a OpenGL para hablar un poco sobre los conceptos básicos que a mí se me complicaron cuando empecé a usarlo. No va a haber nada de código, eso lo dejaré para un futuro post. Por ahora quiero explicar los conceptos básicos de OpenGL para entender cómo funciona.

¿Qué es OpenGL y por qué usarlo?

OpenGL es una API de render que te deja comunicarte con la GPU para crear todo tipo de gráficos, ya sea de videojuegos, aplicaciones, etc. OpenGL funciona como una Máquina de Estado, lo que significa que para hacer cualquier cosa necesitarás modificar, leer o eliminar alguno de los estados de OpenGL.

Es un poco fastidioso desde mi punto de vista, pero al menos es menos verboso que otras APIs como Vulkan, lo cual lo hace un buen candidato para empezar en el maravilloso (y frustrante…) mundo de la programación de gráficos (o graphics programming).

A veces puede llegar a ser fastidioso para debuggear ya que necesitar que estar preguntandole a OpenGL sobre los errores que puedan haber, pero hay maneras de lidiar con ello. OpenGL y en general todas las APIs de rendering tienen sus propios Render Pipeline, veamos un poco cómo funciona el de OpenGL.

¿En qué consiste el Render Pipeline de OpenGL?

Un “Render Pipeline” no es más que la serie de pasos para mostrar cosas en la pantalla. De ahí viene el nombre “pipeline”, como si fuera agua que va pasando por una serie de tuberías, solo que en este caso son los datos de las imagenes que queremos renderizar, los cuales pasan por diferentes procesos hasta ser mostrados en pantalla. Veamos cuáles son estos pasos.

Vertex Shader

Primero que nada, OpenGL empieza tomando los datos que le pasamos, los almacena en la GPU por medio de la VRAM para poder usarlos en un shader llamado Vertex Shader. Este se ejecuta una vez por cada vértice. Los “vértices”, en este contexto, no son más que la coordenada de cada punto de la forma geométrica que vamos a representar, y que luego serán usados para crear las áreas que se van a renderizar.

Aunque los vértices pueden contener más datos, usualmente son coordenadas para definir la posición de estos puntos.

Definición de la geometría

Luego estos puntos se unen para definir cuál va a ser la geometría que vamos a mostrar, aparte de algunos pasos opcionales que también influyen en la geometría del área renderizada, pero que no tocaremos en este post.

Rasterización

Aquí se forman los píxeles que luego mostrarán los colores necesarios para formar todo tipo de imágenes. Estos píxeles usualmente están dentro del área de la forma definida anteriormente.

Fragment Shader

Un tipo de shader distinto del Vertex Shader. Este se ejecuta una vez por “fragmento”, que son generados en la etapa de rasterización, es decir, los píxeles prácticamente. En este shader es donde podremos mostrar los colores de cada píxel para generar imágenes. Esto, por supuesto, también incluye leer los datos de una imagen (textura) para poder mostrarlos en el área generada anteriormente.

Operaciones por fragmento

Finalmente, luego del Fragment Shader, vienen todas las operaciones adicionales para chequear los fragmentos generados anteriormente y aplicar efectos adicionales, como mezcla de colores en caso de que haya alguno con transparencia, etc.

Obviamente, esta es una explicación resumida de lo que en realidad ocurre en cada etapa. Les dejo este link para aprender mas sobre este tema. Ahora que entendemos los pasos que OpenGL ejecuta para renderizar cosas, veamos como usa los datos que le pasamos en esta proceso.

Buffers, Vértices y representación de datos

En general, la mayoría del tiempo al usar OpenGL vamos a estar haciendo uso de Vertex Buffers y Vertex Attributes. Esta es la forma en la que OpenGL transfiere datos del CPU al GPU, y luego serán usados por los shaders en las etapas mencionadas para generar las imágenes.

Los Vertex Buffers no son más que los datos que almacenamos en memoria, como por ejemplo, un array de números con las coordenadas de cada punto en nuestra forma geométrica. Y los Vertex Attributes son la forma en la que OpenGL interpreta estos datos. Un par de números en un array puede ser representado no solo como coordenadas, sino también como colores y básicamente cualquier dato que necesitemos. Al trabajar con OpenGL, tendremos que decirle cómo interpretar los buffers para saber cuántos Vertex Attributes va a tener y qué rol tiene cada uno.

Los shaders toman cada uno de los Vertex Attributes y hacen cosas con ellos. Como, por ejemplo, definir la posición en la que se va a mostrar cada lado de una forma geométrica (la primera etapa de la pipeline) y el color que va a tener el área de esta forma geométrica (en el fragment shader). De esta manera podremos visualizar cosas.

Flujo de trabajo con OpenGL

Finalmente, y para concluir el post, veamos más o menos como sería el flujo de trabajo con OpenGL a la hora de renderizar cosas para un videojuego, por ejemplo.

Por lo general vamos a empezar un proyecto de OpenGL definiendo las configuraciones iniciales tanto de la librería para manejo de ventanas e input del usuario como de OpenGL en sí. El tamaño de pantalla, el perfil de OpenGL que vamos a usar, etc. Librerías como GLFW o SDL son de las mas usadas junto con OpenGL.

Luego, en algún punto, según lo que queramos renderizar, vamos a necesitar hacer uso de los Vertex Buffers y Vertex Attributes para definir un área en donde vayamos a mostrar nuestros objetos.

Lo que se suele hacer es abstraer este tipo de tareas en distintas estructuras de datos, por ejemplo, una clase Render con métodos para modificar los buffers y vertices, una clase Texture, Shader, etc. Estas abstracciones son las que usarán las funciones de OpenGL por debajo para hacer todo.

Una vez que tengamos los datos (un ID en la mayoria de las casos) de los shaders luego de compilarlos con OpenGL, podremos pasarles los datos de nuestros vértices y texturas para dibujar los puntos del área que renderizaremos y con nuestro Fragment Shader asignaremos a cada pixel el color correspondiente a las imagenes que vamos a mostrar. Este tipo de cosas se suelen hacer con uniforms y layouts, que los veremos en futuros posts.

Así que básicamente estaremos constantemente dentro de un bucle, modificando nuestros buffers y vértices, pasando datos a los shaders, y posiblemente también compilando nuevos shaders y cargando nuevas texturas.

Ya todo lo demás es cuestión de modificar todos estos parámetros para generar formas geométricas, movimiento y todo lo que necesitemos y aumentando la complejidad en el proceso. Luego, en un futuro, veremos cómo aplicar todo esto en código y renderizar un triángulo.

¡Muchas gracias por leer, hasta la próxima!