Motor con OpenGL + SDL (I)

OpenGL + SDL engine

Aviso: este es el primer post que he escrito nunca, así que, si tienes alguna sugerencia o crítica respecto al formato del texto, mi escritura o cuestiones técnicas, házmelo saber, gracias.

Este post en particular trata los pequeños baches que tuve que sortear al comenzar a desarrollar el engine con OpenGL y la librería SDL desde el inicio, aunque mientras estaba escribiendo el título me di cuenta que podía ser más ambicioso y continuar relatando el desarrollo del engine en una especie de serie de posts (por eso el (I) en el título).

Como he dicho antes, voy a enumerar una serie de cuestiones que encuentro relevantes para que la gente tenga un inicio menos frustrante al comenzar su propio engine con OpenGL y SDL. Me gustaría que sirviera para ayudar a resolver esas pequeñas “tonterías” que nos lastran y nos quitan tiempo de desarrollar cosas más importantes.

Intentaré ordenar los temas cronológicamente (desde el comienzo del proyecto en adelante):

Linkado estático o dinámico para el engine

Elige estático, fin.

No, en serio, elegir linkado estático es más fácil y menos coñazo que el dinámico. Vamos a ver el porqué:

Si eliges linkado dinámico tendrás que marcar como exportable cada clase, tipo y variable global en tu proyecto que quieras exponer al usuario. Quizá no veas todavía lo que implica, a parte de lo obvio, y es que si expones instancias de clases templatizadas, cada una de estas tendrá que ser marcada como exportable, además, si es un tipo de las STL el usuario tendría que usar el mismo compilador y runtime que tu librería, incluso hay contenedores de las STL que ni siquiera se pueden exportar por uso de clases templatizadas internamente.
También tenemos el problema del “dll hell” en Windows, se denomina así al hecho que cada vez que se instala un programa, los dll’s que utiliza se quedan guardados en el registro y si se instala un nuevo programa que utiliza los mismos dll’s, estos sobreescriben a los anteriores. Como consecuencia el primer programa podría dejar de funcionar correctamente por no tener disponible una versión de los dll’s para los que había sido pensado el desarrollo.
Por supuesto estos problemas se pueden solucionar con diversas técnicas pero quizá el esfuerzo no merezca la pena sopesando los beneficios del linkado dinámico.

Por otro lado, si eliges linkado estático, evitas estos dolores de cabeza. De acuerdo en que tu build final será más grande y tendrás que buildear el proyecto cada vez que cambies el código del engine (a diferencia de como funciona el dinámico) pero creo que las ventajas del linkado dinámico (librerías compartidas entre múltiples programas y carga en ejecución) no se adecúan a los propósitos de un proyecto de motor de videojuegos.

De todas formas, al final, tú tienes que decidir lo que mejor se adapte a tus necesidades.

ERRORES DE LINKADO CONTRA SDL

Pueden ser muy frustrantes a veces los errores de linkado, pero si sabes cómo funciona el proceso de linkado y te paras a pensar sobre el error que estás recibiendo no deberían ser tan difíciles de resolver.

De todas formas en mi caso un error de linkado que me hizo perder mi precioso tiempo intentando arreglarlo fue uno relacionado con las llamadas a funciones de la librería SDL. Yo no veía por qué estaba recibiendo esos errores. Después de un rato probé por si acaso a volver a linkar la librería de SDL en las settings de Visual Studio, asegurándome de poner las versiones correctas en las diferentes configuraciones (x86/x64). Resulta que funcionó, así que el problema era que me confundí al linkar por primera vez la librería.

Merece la pena recordar comprobar dos veces si cada configuración del proyecto linka contra la versión correcta de la librería.

La ventana no responde

Si la ventana de tu aplicación no responde, es decir, no puedes moverla, tampoco cambiar el tamaño ni responde de ninguna manera al ratón entonces es que necesitas añadir las siguientes líneas en algún punto dentro del bucle del frame:

// Poll input events
SDL_Event e; 
while (SDL_PollEvent(&e)) 
{ 
    if (e.type == SDL_QUIT) 
    {
        // Exit app
    }
}

Después de esto recuperarás la vida de tu ventana. Este bucle te alimenta con todos los eventos de input que se han producido durante el frame, después os puedes gestionar como quieras.

Las llamadas a funciones de glew fallan

Primero de todo, yo uso GLEW como mi wrapper para llamadas a OpenGL.

Una vez que tuve el proyecto compilando de nuevo otro problema apreció: las llamadas a funciones de OpenGL eran ignoradas o crasheaban. Principalmente el problema radica en el orden de inicialización, este es el que funciona para mí:

  1. SDL_Init.
  2. Establecemos los atributos de OpenGL.
  3. SDL_CreateWindow > SDL_CreateContext > SDL_GL_MakeCurrent.
  4. SDL_SetSwapInterval(int).
  5. glewInit().

GLEW necesita un contexto gráfico antes de inicializarse. También, si glewInit() falla, las siguientes llamadas a funciones de OpenGL no tendrán efecto o crashearan el programa.

Cazando errores

Es importante usar las herramientas de debug o especificaciones para resolver (o encontrar lol!) las cosas que funcionan mal en nuestro software.
A veces quizá no te des cuenta de todo lo que está a nuestra disposición para su uso, bien, en este caso, yo uso “glGetError” para seguir la pista a errores de OpenGL en mi código. Suelo ponerlo después de llamadas sensibles de causar errores o en general tan solo por seguridad. Esta spec funciona como un stack así que cada vez que la llames te devolverá el último error guardado.
Del lado de la SDL tenemos “SDL_GetError”, la cual funciona similar pero solo guarda un único error (el último).

También uso RenderDoc, el cual es muy útil para ver de un vistazo tu pipeline de dibujado frame a frame: buffers gráficos, cambios en el render context, inspeccionar drawcalls, valores de los atributos de los vértices, geometría de primitivas…incluso puedes hacer live-editing de los shaders (del fragment shader al menos). Una característica que me gusta mucho pero que no me funciona es el “historial de pixel”, el cual te muestra el historial de las drawcall que han afectado a cierto pixel. Tampoco me funciona el debug de los shaders, no sé si tiene que ver con que el soporte a OpenGL por parte de RenderDoc es relativamente nuevo o quizá estoy haciendo algo mal. De todas formas, personalmente me quedo con el debbuger gráfico que trae Visual Studio, no tiene soporte para OpenGL pero funciona de maravilla con Direct3D.

Algunos objectos se renderizan visualmente igual a pesar de que tienen diferente configuración de dibujado

Me pasó que los sprites se renderizaban todos con la misma forma cuando cada uno de ellos tenían diferentes tamaños. Lo que estaba ocurriendo es que todo compartían el mismo VBO por alguna razón. Descubrí que el VBO del último sprite actualizado se estaba vinculando a los VAO’s del resto de sprites porque yo no desvinculaba los VBO’s al dejar de utilizarlos. En verdad es por culpa de mi arquitectura en la pipeline de dibujado la cual es: bind VAO > bind y actualizar VBO solo si los valores de atributos de los vértices han cambiado > llamar a glVertexAttribPointer cada frame. Te puedes dar cuenta que siempre queda colgando un VBO vinculado, y entonces cuando llamo a glVertexAttribPointer, el VBO, se vincula al VAO activo :(.

Obviamente, tengo que rehacer el flow, pero por ahora solucioné temporalmente el problema desvinculando el VBO después de glVertexAttribPointer. También compruebo que haya un buffer vinculado antes de llamar a la función, de otra manera obtendríamos un comportamiento indefinido.

Aquí lo importante es recordar desvincular los recursos una vez que hayas acabado de trabajar con ellos. Siempre es una mala práctica dejar cosas colgando en tu código, es muy probable que acaben dando problemas y es más difícil de descubrirlos después.

Imágenes jpg con ancho impar se ven mal

Cuando intenté dibujar un sprite con una imagen JPG el resultado fue que se veía mal en pantalla, en escala de grises y con una raya diagonal cruzando la imagen. No se veía como debería y se supone que tengo soporte para texturas con formato 24bpp RGB.
Descubrí, buceando en la documentación sobre texturas de OpenGL, que puedes indicar el alignment de memoria que usará la GPU para leer el buffer de píxeles de la memoria del cliente. Por defecto tiene un alignment de 4 bytes. Esto significa que el tamaño total en bytes de una fila de píxeles de tu textura debe ser múltiplo de 4, se vería mal de otra manera.

Así que para formatos con número par de bytes por píxel tienes que usar 4 bytes alignment.

glPixelStorei(GL_UNPACK_ALIGNMENT, 4);

Y para texturas con ancho impar y formato con número impar de bytes por píxel tienes que usar 1 byte alignment.

glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

Deja un comentario

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

Este sitio usa Akismet para reducir el spam. Conoce cómo se procesan los datos de tus comentarios.