OpenGL avanzado en Python con PyGame y PyOpenGL

     

    Introducci贸n

    Siguiendo el art铆culo anterior, Comprender OpenGL a trav茅s de Python, donde hemos establecido las bases para un mayor aprendizaje, podemos saltar a OpenGL utilizando PyGame y PyOpenGL.

    PyOpenGL es la biblioteca estandarizada que se usa como puente entre Python y las API de OpenGL, y PyGame es una biblioteca estandarizada que se usa para crear juegos en Python. Ofrece bibliotecas de audio y gr谩ficos 煤tiles incorporadas y lo usaremos para representar el resultado m谩s f谩cilmente al final del art铆culo.

    Como se mencion贸 en el art铆culo anterior, OpenGL es muy antiguo, por lo que no encontrar谩 muchos tutoriales en l铆nea sobre c贸mo usarlo y comprenderlo correctamente, porque todos los principales expertos ya est谩n familiarizados con las nuevas tecnolog铆as.

    En este art铆culo, veremos varios temas fundamentales que necesitar谩 saber:

    • Inicializar un proyecto con PyGame
    • Objetos de dibujo
    • Animaci贸n iterativa
    • Utilizando matrices de transformaci贸n
    • Ejecuci贸n de transformaci贸n m煤ltiple
    • Ejemplo de implementaci贸n

    Inicializar un proyecto con PyGame

    En primer lugar, necesitamos instalar PyGame y PyOpenGL si a煤n no lo ha hecho:

    $ python3 -m pip install -U pygame --user
    $ python3 -m pip install PyOpenGL PyOpenGL_accelerate
    

    Nota: Puede encontrar una instalaci贸n m谩s detallada en el art铆culo anterior de OpenGL.

    Si tiene problemas con la instalaci贸n, PyGame “Empezando” La secci贸n puede ser un buen lugar para visitar.

    Dado que no tiene sentido descargar 3 libros de teor铆a gr谩fica sobre ti, usaremos la biblioteca PyGame para darnos una ventaja. B谩sicamente, solo acortar谩 el proceso desde la inicializaci贸n del proyecto hasta el modelado y la animaci贸n reales.

    Para empezar, necesitamos importar todo lo necesario tanto de OpenGL como de PyGame:

    import pygame as pg
    from pygame.locals import *
    
    from OpenGL.GL import *
    from OpenGL.GLU import *
    

    A continuaci贸n, llegamos a la inicializaci贸n:

    pg.init()
    windowSize = (1920,1080)
    pg.display.set_mode(display, DOUBLEBUF|OPENGL)
    

    Si bien la inicializaci贸n es solo de tres l铆neas de c贸digo, cada una merece al menos una explicaci贸n simple:

    • pg.init(): Inicializaci贸n de todos los m贸dulos de PyGame: esta funci贸n es un regalo del cielo
    • windowSize = (1920, 1080): Definici贸n de un tama帽o de ventana fijo
    • pg.display.set_mode(display, DOUBLEBUF|OPENGL): Aqu铆, especificamos que usaremos OpenGL con doble b煤fer

    El almacenamiento en b煤fer doble significa que hay dos im谩genes en un momento dado: una que podemos ver y otra que podemos transformar como mejor nos parezca. Vemos el cambio real causado por las transformaciones cuando se intercambian los dos b煤feres.

    Ya que tenemos configurada nuestra ventana gr谩fica, a continuaci贸n debemos especificar qu茅 veremos, o m谩s bien d贸nde se colocar谩 la “c谩mara” y qu茅 tan lejos y ancho puede ver.

    Esto se conoce como tronco – que es solo una pir谩mide recortada que representa visualmente la vista de la c谩mara (lo que puede y no puede ver).

    Un frustum se define mediante 4 par谩metros clave:

    • El FOV (campo de visi贸n): 脕ngulo en grados
    • La relaci贸n de aspecto: Definido como la relaci贸n entre el ancho y el alto
    • La coordenada z del plano de recorte cercano: La distancia m铆nima de dibujo
    • La coordenada z del plano de recorte lejano: La distancia m谩xima de dibujo

    Entonces, continuemos e implementemos la c谩mara con estos par谩metros en mente, usando c贸digo OpenGL C:

    void gluPerspective(GLdouble fovy, GLdouble aspect, GLdouble zNear, GLdouble zFar);
    gluPerspective(60, (display[0]/display[1]), 0.1, 100.0)
    

    Para comprender mejor c贸mo funciona un frustum, aqu铆 hay una imagen de referencia:

    Los aviones cercanos y lejanos se utilizan para un mejor rendimiento. Siendo realistas, renderizar cualquier cosa fuera de nuestro campo de visi贸n es un desperdicio de rendimiento de hardware que podr铆a usarse para renderizar algo que realmente podemos ver.

    Entonces, todo lo que el jugador no puede ver se almacena impl铆citamente en la memoria, aunque no est茅 visualmente presente. Aqu铆 est谩 un gran video de c贸mo se ve el renderizado solo dentro del frustum.

    Objetos de dibujo

    Despu茅s de esta configuraci贸n, imagino que nos estamos haciendo la misma pregunta:

    Bueno, todo esto est谩 muy bien, pero 驴c贸mo puedo hacer un Super Destructor Estelar?

    Bueno … con puntos. Cada modelo en el objeto OpenGL se almacena como un conjunto de v茅rtices y un conjunto de sus relaciones (qu茅 v茅rtices est谩n conectados). Entonces, te贸ricamente, si supieras la posici贸n de cada punto que se usa para dibujar un Super Destructor Estelar, 隆podr铆as dibujar uno!

    Hay algunas formas en que podemos modelar objetos en OpenGL:

    • Dibujando usando v茅rtices, y dependiendo de c贸mo OpenGL interprete estos v茅rtices, podemos dibujar con:
      • puntos: como en puntos literales que no est谩n conectados de ninguna manera
      • l铆neas: cada par de v茅rtices construye una l铆nea conectada
      • triangulos: cada tres v茅rtices forma un tri谩ngulo
      • cuadril谩tero: cada cuatro v茅rtices forma un cuadril谩tero
      • pol铆gono: t煤 entiendes
      • mucho mas…
    • Dibujar usando las formas y objetos incorporados que fueron cuidadosamente modelados por los colaboradores de OpenGL
    • Importaci贸n de objetos completamente modelados

    Entonces, para dibujar un cubo, por ejemplo, primero necesitamos definir sus v茅rtices:

    cubeVertices = ((1,1,1),(1,1,-1),(1,-1,-1),(1,-1,1),(-1,1,1),(-1,-1,-1),(-1,-1,1),(-1, 1,-1))
    

    Luego, necesitamos definir c贸mo est谩n todos conectados. Si queremos hacer un cubo de alambre, necesitamos definir las aristas del cubo:

    cubeEdges = ((0,1),(0,3),(0,4),(1,2),(1,7),(2,5),(2,3),(3,6),(4,6),(4,7),(5,6),(5,7))
    

    Esto es bastante intuitivo: el punto 0 tiene una ventaja con 1, 3y 4. El punto 1 tiene un borde con puntos 3, 5y 7, y as铆.

    Y si queremos hacer un cubo s贸lido, entonces necesitamos definir los cuadril谩teros del cubo:

    cubeQuads = ((0,3,6,4),(2,5,6,3),(1,2,5,7),(1,0,4,7),(7,4,6,5),(2,3,0,1))
    

    Esto tambi茅n es intuitivo: para hacer un cuadril谩tero en la parte superior del cubo, querr铆amos “colorear” todo entre los puntos 0, 3, 6y 4.

    Tenga en cuenta que hay una raz贸n real por la que etiquetamos los v茅rtices como 铆ndices de la matriz en la que est谩n definidos. Esto hace que escribir c贸digo que los conecte sea muy f谩cil.

    La siguiente funci贸n se utiliza para dibujar un cubo cableado:

    def wireCube():
        glBegin(GL_LINES)
        for cubeEdge in cubeEdges:
            for cubeVertex in cubeEdge:
                glVertex3fv(cubeVertices[cubeVertex])
        glEnd()
    

    glBegin() es una funci贸n que indica que definiremos los v茅rtices de una primitiva en el siguiente c贸digo. Cuando terminamos de definir la primitiva, usamos la funci贸n glEnd().

    GL_LINES es una macro que indica que dibujaremos l铆neas.

    glVertex3fv() es una funci贸n que define un v茅rtice en el espacio, hay algunas versiones de esta funci贸n, as铆 que en aras de la claridad, veamos c贸mo se construyen los nombres:

    • glVertex: una funci贸n que define un v茅rtice
    • glVertex3: una funci贸n que define un v茅rtice usando 3 coordenadas
    • glVertex3f: una funci贸n que define un v茅rtice usando 3 coordenadas de tipo GLfloat
    • glVertex3fv: una funci贸n que define un v茅rtice usando 3 coordenadas de tipo GLfloat que se colocan dentro de un vector (tupla) (la alternativa ser铆a glVertex3fl que usa una lista de argumentos en lugar de un vector)

    Siguiendo una l贸gica similar, se utiliza la siguiente funci贸n para dibujar un cubo s贸lido:

    def solidCube():
        glBegin(GL_QUADS)
        for cubeQuad in cubeQuads:
            for cubeVertex in cubeQuad:
                glVertex3fv(cubeVertices[cubeVertex])
        glEnd()
    

    Animaci贸n iterativa

    Para que nuestro programa sea “eliminable”, debemos insertar el siguiente fragmento de c贸digo:

    for event in pg.event.get():
        if event.type == pg.QUIT:
            pg.quit()
            quit()
    

    B谩sicamente es solo un oyente que se desplaza por los eventos de PyGame y, si detecta que hicimos clic en el bot贸n “cerrar ventana”, cierra la aplicaci贸n.

    Cubriremos m谩s eventos de PyGame en un art铆culo futuro; este se present贸 de inmediato porque ser铆a bastante inc贸modo para los usuarios y para ustedes mismos tener que iniciar el administrador de tareas cada vez que quieran salir de la aplicaci贸n.

    En este ejemplo, usaremos b煤fer doble, lo que solo significa que usaremos dos b煤feres (puede pensar en ellos como lienzos para dibujar) que se intercambiar谩n en intervalos fijos y dar谩n la ilusi贸n de movimiento.

    Sabiendo esto, nuestro c贸digo debe tener el siguiente patr贸n:

    handleEvents()
    glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
    doTransformationsAndDrawing()
    pg.display.flip()
    pg.time.wait(1)
    
    • glClear: Funci贸n que borra los b煤feres especificados (lienzos), en este caso, el b煤fer de color (que contiene informaci贸n de color para dibujar los objetos generados) y el b煤fer de profundidad (un b煤fer que almacena las relaciones delante o detr谩s de todos los objetos generados).
    • pg.display.flip(): Funci贸n que actualiz贸 la ventana con el contenido del b煤fer activo
    • pg.time.wait(1): Funci贸n que detiene el programa por un per铆odo de tiempo

    glClear tiene que ser usado porque si no lo usamos, solo estaremos pintando sobre un lienzo ya pintado, que en este caso es nuestra pantalla y vamos a terminar con un l铆o.

    A continuaci贸n, si queremos actualizar continuamente nuestra pantalla, como una animaci贸n, tenemos que poner todo nuestro c贸digo dentro de un while bucle en el que nosotros:

    • Manejar eventos (en este caso, simplemente saliendo)
    • Limpiar las zonas de influencia de color y profundidad para que se puedan volver a dibujar
    • Transforma y dibuja objetos
    • Actualizar la pantalla
    • GOTO 1.

    El c贸digo deber铆a verse as铆:

    while True:
        handleEvents()
        glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
        doTransformationsAndDrawing()
        pg.display.flip()
        pg.time.wait(1)
    

    Utilizando matrices de transformaci贸n

    En el art铆culo anterior explicamos c贸mo, en teor铆a, necesitamos construir una transformaci贸n que tenga un punto de referencia.

    OpenGL funciona de la misma manera, como se puede ver en el siguiente c贸digo:

    glTranslatef(1,1,1)
    glRotatef(30,0,0,1)
    glTranslatef(-1,-1,-1)
    

    En este ejemplo, hicimos una rotaci贸n del eje z en el plano xy con el centro de rotaci贸n siendo (1,1,1) por 30 grados.

    Repasemos un poco si estos t茅rminos suenan un poco confusos:

    • La rotaci贸n del eje z significa que estamos girando alrededor del eje z

      Esto solo significa que estamos aproximando un plano 2D con un espacio 3D, toda esta transformaci贸n es b谩sicamente como hacer una rotaci贸n normal alrededor de un punto de referencia en el espacio 2D.

    • Obtenemos el plano xy aplastando todo un espacio 3D en un plano que tiene z=0 (eliminamos el par谩metro z en todos los sentidos)
    • El centro de rotaci贸n es un v茅rtice alrededor del cual giraremos un objeto dado (el centro de rotaci贸n predeterminado es el v茅rtice de origen (0,0,0))

    Pero hay un problema: OpenGL entiende el c贸digo anterior recordando y modificando constantemente una matriz de transformaci贸n global.

    Entonces, cuando escribe algo en OpenGL, lo que est谩 diciendo es:

    # This part of the code is not translated
    # transformation matrix = E (neutral)
    glTranslatef(1,1,1)
    # transformation matrix = TxE
    # ALL OBJECTS FROM NOW ON ARE TRANSLATED BY (1,1,1)
    

    Como puede imaginar, esto plantea un gran problema, porque a veces queremos utilizar una transformaci贸n en un solo objeto, no en todo el c贸digo fuente. Esta es una raz贸n muy com煤n de errores en OpenGL de bajo nivel.

    Para combatir esta caracter铆stica problem谩tica de OpenGL, se nos presentan matrices de transformaci贸n de empuje y estallido: glPushMatrix() y glPopMatrix():

    # Transformation matrix is T1 before this block of code
    glPushMatrix()
    glTranslatef(1,0,0)
    generateObject() # This object is translated
    glPopMatrix()
    generateSecondObject() # This object isn't translated
    

    Estos funcionan de una manera simple 脷ltimo en entrar primero en salir (LIFO) principio. Cuando deseamos realizar una traducci贸n a una matriz, primero la duplicamos y luego la colocamos encima de la pila de matrices de transformaci贸n.

    En otras palabras, a铆sla todas las transformaciones que estamos realizando en este bloque creando una matriz local que podemos desechar una vez que hayamos terminado.

    Una vez que se traduce el objeto, sacamos la matriz de transformaci贸n de la pila, dejando el resto de las matrices intactas.

    Ejecuci贸n de transformaci贸n m煤ltiple

    En OpenGL, como se mencion贸 anteriormente, las transformaciones se agregan a la matriz de transformaci贸n activa que est谩 en la parte superior de la pila de matrices de transformaci贸n.

    Esto significa que las transformaciones se ejecutan en orden inverso. Por ejemplo:

    ######### First example ##########
    glTranslatef(-1,0,0)
    glRotatef(30,0,0,1)
    drawObject1()
    ##################################
    
    ######## Second Example #########
    glRotatef(30,0,0,1)
    glTranslatef(-1,0,0)
    drawObject2()
    #################################
    

    En este ejemplo, Object1 se gira primero, luego se traduce, y Object2 primero se traduce y luego se gira. Los dos 煤ltimos conceptos no se usar谩n en el ejemplo de implementaci贸n, pero se usar谩n pr谩cticamente en el pr贸ximo art铆culo de la serie.

    Ejemplo de implementaci贸n

    El siguiente c贸digo dibuja un cubo s贸lido en la pantalla y lo gira continuamente 1 grado alrededor del (1,1,1) vector. Y se puede modificar muy f谩cilmente para dibujar un cubo de alambre intercambiando el cubeQuads con el cubeEdges:

    import pygame as pg
    from pygame.locals import *
    
    from OpenGL.GL import *
    from OpenGL.GLU import *
    
    cubeVertices = ((1,1,1),(1,1,-1),(1,-1,-1),(1,-1,1),(-1,1,1),(-1,-1,-1),(-1,-1,1),(-1,1,-1))
    cubeEdges = ((0,1),(0,3),(0,4),(1,2),(1,7),(2,5),(2,3),(3,6),(4,6),(4,7),(5,6),(5,7))
    cubeQuads = ((0,3,6,4),(2,5,6,3),(1,2,5,7),(1,0,4,7),(7,4,6,5),(2,3,0,1))
    
    def wireCube():
        glBegin(GL_LINES)
        for cubeEdge in cubeEdges:
            for cubeVertex in cubeEdge:
                glVertex3fv(cubeVertices[cubeVertex])
        glEnd()
    
    def solidCube():
        glBegin(GL_QUADS)
        for cubeQuad in cubeQuads:
            for cubeVertex in cubeQuad:
                glVertex3fv(cubeVertices[cubeVertex])
        glEnd()
    
    def main():
        pg.init()
        display = (1680, 1050)
        pg.display.set_mode(display, DOUBLEBUF|OPENGL)
    
        gluPerspective(45, (display[0]/display[1]), 0.1, 50.0)
    
        glTranslatef(0.0, 0.0, -5)
    
        while True:
            for event in pg.event.get():
                if event.type == pg.QUIT:
                    pg.quit()
                    quit()
    
            glRotatef(1, 1, 1, 1)
            glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
            solidCube()
            #wireCube()
            pg.display.flip()
            pg.time.wait(10)
    
    if __name__ == "__main__":
        main()
    

    Al ejecutar este fragmento de c贸digo, aparecer谩 una ventana de PyGame, renderizando la animaci贸n del cubo:

    Conclusi贸n

    Hay mucho m谩s que aprender sobre OpenGL: iluminaci贸n, texturas, modelado avanzado de superficies, animaci贸n modular compuesta y mucho m谩s.

    Pero no se preocupe, todo esto se explicar谩 en los siguientes art铆culos que ense帽an al p煤blico sobre OpenGL de la manera correcta, desde cero.

    Y no te preocupes, en el pr贸ximo art铆culo dibujaremos algo semi-decente.

    .

    Etiquetas:

    Deja una respuesta

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