Entendiendo OpenGL a través de Python

OpenGL es antiguo y no hay mucha literatura en línea para aprender. Comprender los conceptos básicos y cómo funcionan es importante para todo, y especialmente cuando se trata de gráficos. En este artículo, comenzaremos desde abajo y cubriremos los temas importantes de OpenGL en Python.

Introducción

Siguiendo Este artículo de Muhammad Junaid Khalid, donde se explican los conceptos básicos y la configuración de OpenGL. explicado, ahora veremos cómo hacer objetos más complejos y cómo animarlos.

OpenGL es muy antiguo, y no encontrará muchos tutoriales en línea sobre cómo usarlo correctamente y entenderlo porque todos los perros principales ya están hasta las rodillas en nuevas tecnologías.

Para comprender el código OpenGL moderno, primero debe comprender los conceptos antiguos que fueron escritos en tablas de piedra por los sabios desarrolladores de juegos mayas.

En este artículo, abordaremos varios temas fundamentales que necesitará saber:

En la última sección, veremos cómo usar OpenGL con las bibliotecas de Python PyGame y PyOpenGL.

En el próximo artículo profundizaremos en cómo usar OpenGL con Python y las bibliotecas mencionadas anteriormente.

Operaciones básicas con matrices

Para poder usar correctamente muchas de las funciones en OpenGL, necesitaremos algo de geometría.

Cada punto en el espacio se puede representar con coordenadas cartesianas. Las coordenadas representan la ubicación de cualquier punto dado definiendo sus valores X, Y y Z.

Los usaremos prácticamente como matrices 1x3, o más bien vectores tridimensionales (más sobre matrices más adelante).

Aquí hay ejemplos de algunas coordenadas:

$$a = (5,3,4)\ b = (9,1,2)$$

a y b son puntos en el espacio, sus coordenadas x son 5 y 9 respectivamente, las coordenadas y son 3 y 1, y así sucesivamente.

En los gráficos por computadora, la mayoría de las veces, se utilizan coordenadas homogéneas en lugar de las antiguas coordenadas cartesianas regulares. Son básicamente lo mismo, solo que con un parámetro de utilidad adicional, que por simplicidad diremos que siempre es 1.

Entonces, si las coordenadas regulares de a son (5,3,4), las coordenadas homogéneas correspondientes serían (5,3,4,1). Hay mucha teoría geométrica detrás de esto, pero no es realmente necesaria para este artículo.

A continuación, una herramienta esencial para representar transformaciones geométricas son las matrices. Una matriz es básicamente un arreglo bidimensional (en este caso de tamaño n*n, es muy importante que tengan el mismo número de filas y columnas).

Ahora bien, las operaciones con matrices son, en la mayoría de los casos, bastante sencillas, como la suma, la resta, etc. Pero, por supuesto, la operación más importante tiene que ser la más complicada: la multiplicación. Echemos un vistazo a los ejemplos básicos de operaciones con matrices:

$$A = \begin{bmatriz} 1 y 2 y 5 \ 6 y 1 y 9 \ 5 y 5 y 2 \ \end{bmatrix} - \text{Ejemplo\matriz}\ \begin{bmatrix} 1 y 2 y 5 \ 6 y 1 y 9 \ 5 y 5 y 2 \ \end{bmatriz} + \begin{bmatriz} 2 y 5 y 10 \ 12 y 2 y 18 \ 10 y 10 y 4 \ \end{bmatriz} = \begin{bmatriz} 3 y 7 y 15 \ 18 y 3 y 27 \ 15 y 15 y 6 \ \end{bmatrix} - \text{matriz\ suma}\ \begin{bmatrix} 2 y 4 y 10 \ 12 y 2 y 18 \ 10 y 10 y 4 \ \end{bmatrix} - \begin{bmatrix} 1 y 2 y 5 \ 6 y 1 y 9 \ 5 y 5 y 2 \ \end{bmatriz} = \begin{bmatriz} 1 y 2 y 5 \ 6 y 1 y 9 \ 5 y 5 y 2 \ \end{bmatrix} - \text{Matriz\ resta}\ $$

Ahora, como todas las matemáticas tienden a hacer, se vuelve relativamente complicado cuando realmente quieres algo práctico.

La fórmula para la multiplicación de matrices es la siguiente:

$$
c[i,j] = \sum_{k=1}^{n}a[i,k]*b[k,j]
$$

c es la matriz resultante, a y b son el multiplicando y el multiplicador.

De hecho, hay una explicación simple para esta fórmula. Cada elemento se puede construir sumando los productos de todos los elementos en la i-ésima fila y la j-ésima columna. Esta es la razón por la cual en a[i,k], la i es fija y la k se usa para iterar a través de los elementos de la fila correspondiente. El mismo principio se puede aplicar a b[k,j].

Sabiendo esto, hay una condición adicional que debe cumplirse para que podamos usar la multiplicación de matrices. Si queremos multiplicar matrices A y B de dimensiones a*b y c*d. El número de elementos en una sola fila en la primera matriz (b) tiene que ser el mismo que el número de elementos en una columna en la segunda matriz (c), para que la fórmula anterior se pueda usar correctamente.

Una muy buena forma de visualizar este concepto es resaltar las filas y columnas cuyos elementos se van a utilizar en la multiplicación de un elemento dado. Imagina las dos líneas resaltadas una sobre la otra, como si estuvieran en la misma matriz.

El elemento donde interceptan es la posición del elemento resultante de la sumatoria de sus productos:

intersección de matriz

La multiplicación de matrices es muy importante porque si queremos explicar la siguiente expresión en términos simples: A*B (A y B son matrices), diríamos:

Estamos transformando A usando B.

Es por esto que la multiplicación de matrices es la herramienta por excelencia para transformar cualquier objeto en OpenGL o geometría en general.

Lo último que necesitas saber sobre la multiplicación de matrices es que tiene un neutral. Esto significa que hay un elemento único (matriz en este caso) E que cuando se multiplica con cualquier otro elemento A no cambia el valor de A, es decir:

$$
(!\existe{E}\ \ \forall{A})\ E*A=A
$$

El signo de exclamación junto con el símbolo existe significa: Existe un elemento único E que...

En caso de multiplicación con enteros normales, ‘E’ tiene el valor de ‘1’. En el caso de matrices, E tiene los siguientes valores en cartesianas normales (E~1~) y coordenadas homogéneas (E~2~) respectivamente:

$$E_{1} = \begin{bmatriz} 1 & 0 & 0 \ 0 & 1 & 0 \ 0 & 0 & 1 \ \end{bmatrix}E_{2} = \begin{bmatrix} 1 y 0 y 0 y 0 \ 0 y 1 y 0 y 0 \ 0 & 0 & 1 & 0 \ 0 & 0 & 0 & 1 \ \end{bmatriz}$$

Cada transformación geométrica tiene su propia matriz de transformación única que tiene un patrón de algún tipo, de los cuales los más importantes son:

  • Traducción
  • Escalado
  • Reflexión
  • Rotación
  • Esquileo

Traducción

La traducción es el acto de mover literalmente un objeto por un vector establecido. El objeto que se ve afectado por la transformación no cambia su forma de ninguna manera, ni cambia su orientación, simplemente se mueve en el espacio (es por eso que la traducción se clasifica como un movimiento transformación).

La traducción se puede describir con la siguiente forma matricial:

$$T = \begin{bmatriz} 1 & 0 & 0 & t_{x} \ 0 & 1 & 0 & t_{y} \ 0 & 0 & 1 & t_{z} \ 0 & 0 & 0 & 1 \ \end{bmatriz}$$

El t-s representa cuánto cambiarán los valores de ubicación x, y y z del objeto.

Entonces, después de transformar cualquier coordenada con la matriz de traducción T, obtenemos:

$$
[x,y,z]*T=[t_x+x,t_y+y,t_z+z]
$$

La traducción se implementa con la siguiente función de OpenGL:

1
void glTranslatef(GLfloat tx, GLfloat ty, GLfloat tz);

Como puede ver, si conocemos la forma de la matriz de traducción, comprender la función de OpenGL es muy sencillo, este es el caso de todas las transformaciones de OpenGL.

No te preocupes por GLfloat, es solo un tipo de datos inteligente para que OpenGL funcione en múltiples plataformas, puedes verlo así:

1
2
3
typedef float GLfloat;
typedef double GLdouble;
typedef someType GLsomeType;

Esta es una medida necesaria porque no todos los sistemas tienen el mismo espacio de almacenamiento para un char, por ejemplo.

Rotación

La rotación es una transformación un poco más complicada, por el simple hecho de que depende de 2 factores:

  • Pivote: Alrededor de qué línea en el espacio 3D (o punto en el espacio 2D) rotaremos
  • Cantidad: Por cuánto (en grados o radianes) rotaremos

Debido a esto, primero necesitamos definir la rotación en un espacio 2D, y para eso necesitamos un poco de trigonometría.

Aquí hay una referencia rápida:

Estas funciones trigonométricas solo se pueden usar dentro de un triángulo rectángulo (uno de los ángulos tiene que ser de 90 grados).

La matriz de rotación base para rotar un objeto en el espacio 2D alrededor del vértice (0,0) por el ángulo A es la siguiente:

$$\begin{bmatriz} {cosA} & {- sinA} & 0 \ {senA} & {cosA} & 0 \ 0 & 0 & 1 \ \end{bmatriz}$$

Una vez más, la 3.ª fila y la 3.ª columna son en caso de que queramos apilar transformaciones de traducción encima de otras transformaciones (que haremos en OpenGL), está bien si no comprende completamente por qué son allí ahora mismo. Las cosas deberían aclararse en el ejemplo de transformación compuesta.

Todo esto estaba en el espacio 2D, ahora pasemos al espacio 3D. En el espacio 3D necesitamos definir una matriz que pueda rotar un objeto alrededor de cualquier línea.

Como dijo una vez un hombre sabio: "¡Mantenlo simple y estúpido!" Afortunadamente, los magos de las matemáticas por una vez lo mantuvieron simple y estúpido.

Cada rotación alrededor de una línea se puede dividir en algunas transformaciones:

  • Rotación alrededor del eje x
  • Rotación alrededor del eje y
  • Rotación alrededor del eje z
  • Traducciones de utilidad (que se abordarán más adelante)

Entonces, las únicas tres cosas que necesitamos construir para cualquier rotación 3D son matrices que representan la rotación alrededor de los ejes x, y y z por un ángulo A:

$$R_{x} = \begin{bmatriz} 1 y 0 y 0 y 0 \ 0 & {cosA} & {- sinA} & 0 \ 0 & {senA} & {cosA} & 0 \ 0 & 0 & 0 & 1 \ \end{bmatrix}R_{y} = \begin{bmatrix} {cosA} & 0 & {senA} & 0 \ 0 y 1 y 0 y 0 \ {- sinA} & 0 & {cosA} & 0 \ 0 & 0 & 0 & 1 \ \end{bmatrix}R_{z} = \begin{bmatrix} {cosA} & {- sinA} & 0 & 0 \ {senA} & {cosA} & 0 & 0 \ 0 & 0 & 1 & 0 \ 0 & 0 & 0 & 1 \ \end{bmatriz}$$

La rotación 3D se implementa con la siguiente función OpenGL:

1
void glRotatef(GLfloat angle, GLfloat x, GLfloat y, GLfloat z);
  • angle: ángulo de rotación en grados (0-360)
  • x,y,z: vector alrededor del cual se ejecuta la rotación

Escalado

Escalar es el acto de multiplicar cualquier dimensión del objeto objetivo por un escalar. Este escalar puede ser <1 si queremos encoger el objeto, y puede ser >1 si queremos agrandar el objeto.

El escalado se puede describir con la siguiente forma matricial:

$$S = \begin{bmatriz} s_{x} & 0 & 0 & 0 \ 0 & s_{y} & 0 & 0 \ 0 & 0 & s_{z} & 0 \ 0 & 0 & 0 & 1 \ \end{bmatriz}$$

s~x~, s~y~, s~z~ son los escalares que se multiplican con los valores x, y y z del objeto de destino.

Después de transformar cualquier coordenada con la matriz de escala S obtenemos:

$$\lbrack x,y,z\rbrack \ast S = \lbrack s_{x} \ast x,s_{y} \ast y,s_{z} \ast z\rbrack$$

Esta transformación es particularmente útil cuando se escala un objeto por factor k (esto significa que el objeto resultante es dos veces más grande), esto se logra configurando s~x~=s~y~=*s~z~ *=k:

$$\lbrack x,y,z\rbrack \ast S = \lbrack s_{x} \ast x,s_{y} \ast y,s_{z} \ast z\rbrack$$

Un caso especial de escalado se conoce como reflexión. Se logra configurando s~x~, s~y~ o s~z~ en -1. Esto solo significa que invertimos el signo de una de las coordenadas del objeto.

En términos más simples, colocamos el objeto al otro lado del eje x, y o z.

Esta transformación se puede modificar para que funcione en cualquier plano de reflexión, pero en realidad no la necesitamos por ahora.

1
void glScalef(GLfloat sx, GLfloat sy, GLfloat sz);

Transformaciones compuestas

Las transformaciones compuestas son transformaciones que constan de más de una transformación básica (enumeradas anteriormente). Las transformaciones A y B se combinan mediante una matriz que multiplica las matrices de transformación correspondientes M_a y M_b.

Esto puede parecer una lógica muy sencilla, sin embargo, hay algunas cosas que pueden ser confusas. Por ejemplo:

  • La multiplicación de matrices no es conmutable:

$$A \ast B \neq B \ast A\ \text{A\ y\ B\ siendo\ matrices}$$

  • Cada una de estas transformaciones tiene una transformación inversa. Una transformación inversa es una transformación que cancela la original:

$$T = \begin{bmatriz} 1 & 0 & 0 & un \ 0 & 1 & 0 & b \ 0 & 0 & 1 & c \ 0 & 0 & 0 & 1 \ \end{bmatriz}T^{- 1} = \begin{bmatriz} 1 & 0 & 0 & {- un} \ 0 & 1 & 0 & {- b} \ 0 & 0 & 1 & {- c} \ 0 & 0 & 0 & 1 \ \end{bmatriz}E = \begin{bmatriz} 1 y 0 y 0 y 0 \ 0 y 1 y 0 y 0 \ 0 & 0 & 1 & 0 \ 0 & 0 & 0 & 1 \ \end{bmatriz}\ \ T \ast T^{- 1} = E$$

  • Cuando queremos hacer una inversa de una transformación compuesta, tenemos que cambiar el orden de los elementos utilizados:

$$(A \ast B \ast C)^{- 1} = C^{- 1} \ast B^{- 1} \ast A^{- 1}$$

El punto es que el orden topológico de la utilización de la matriz es muy importante, al igual que ascender a un determinado piso de un edificio.

Si estás en el primer piso y quieres llegar al cuarto piso, primero debes ir al tercer piso y luego al cuarto.

Pero si quiere volver a descender al segundo piso, entonces tendrá que ir al tercer piso y luego al segundo piso (en orden topológico inverso).

Transformaciones que involucran un punto de referencia {#transformaciones que involucran un punto de referencia}

Como se mencionó anteriormente, cuando se debe realizar una transformación relativa a un punto específico en el espacio, por ejemplo, rotando alrededor de un punto de referencia A=(a,b,c) en el espacio 3D, no el origen O=(0, 0,0), necesitamos convertir ese punto de referencia A en O traduciendo todo por T(-a,-b,-c).

Entonces podemos hacer cualquier transformación que necesitemos hacer, y cuando hayamos terminado, traduzca todo de nuevo por T(a,b,c), de modo que el origen original O nuevamente tenga las coordenadas (0, 0,0).

La forma matricial de este ejemplo es:

$$T \ast M \ast T^{- 1} = \begin{bmatriz} 1 & 0 & 0 & {- un} \ 0 & 1 & 0 & {- b} \ 0 & 0 & 1 & {- c} \ 0 & 0 & 0 & 1 \ \end{bmatrix} \ast M \ast \begin{bmatrix} 1 & 0 & 0 & un \ 0 & 1 & 0 & b \ 0 & 0 & 1 & c \ 0 & 0 & 0 & 1 \ \end{bmatriz}$$

Donde M es la transformación que deseamos hacer en un objeto.

El objetivo de aprender estas operaciones matriciales es que pueda comprender completamente cómo funciona OpenGL.

Demostración de modelado

Con todo eso fuera del camino, echemos un vistazo a una demostración de modelado simple.

Para hacer cualquier cosa con OpenGL a través de Python, usaremos dos módulos: PyGame y PyOpenGL:

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

Debido a que es redundante descargar 3 libros de teoría de gráficos, usaremos la biblioteca PyGame. Esencialmente, 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:

1
2
3
4
5
import pygame as pg
from pygame.locals import *

from OpenGL.GL import *
from OpenGL.GLU import *

En el siguiente ejemplo, podemos ver que para modelar un objeto no convencional, todo lo que necesitamos saber es cómo el objeto complejo se puede dividir en partes más pequeñas y simples.

Debido a que aún no sabemos qué hacen algunas de estas funciones, daré algunas definiciones de nivel de superficie en el código mismo, solo para que pueda ver cómo se puede usar OpenGL. En el próximo artículo, se cubrirán todos estos detalles; esto es solo para darle una idea básica de cómo se ve trabajar con OpenGL:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
def draw_gun():
    # Setting up materials, ambient, diffuse, specular and shininess properties are all
    # different properties of how a material will react in low/high/direct light for
    # example.
    ambient_coeffsGray = [0.3, 0.3, 0.3, 1]
    diffuse_coeffsGray = [0.5, 0.5, 0.5, 1]
    specular_coeffsGray = [0, 0, 0, 1]
    glMaterialfv(GL_FRONT, GL_AMBIENT, ambient_coeffsGray)
    glMaterialfv(GL_FRONT, GL_DIFFUSE, diffuse_coeffsGray)
    glMaterialfv(GL_FRONT, GL_SPECULAR, specular_coeffsGray)
    glMateriali(GL_FRONT, GL_SHININESS, 1)

    # OpenGL is very finicky when it comes to transformations, for all of them are global,
    # so it's good to seperate the transformations which are used to generate the object
    # from the actual global transformations like animation, movement and such.
    # The glPushMatrix() ----code----- glPopMatrix() just means that the code in between
    # these two functions calls is isolated from the rest of your project.
    # Even inside this push-pop (pp for short) block, we can use nested pp blocks,
    # which are used to further isolate code in it's entirety.
    glPushMatrix()

    glPushMatrix()
    glTranslatef(3.1, 0, 1.75)
    glRotatef(90, 0, 1, 0)
    glScalef(1, 1, 5)
    glScalef(0.2, 0.2, 0.2)
    glutSolidTorus(0.2, 1, 10, 10)
    glPopMatrix()

    glPushMatrix()
    glTranslatef(2.5, 0, 1.75)
    glScalef(0.1, 0.1, 1)
    glutSolidCube(1)
    glPopMatrix()

    glPushMatrix()
    glTranslatef(1, 0, 1)
    glRotatef(10, 0, 1, 0)
    glScalef(0.1, 0.1, 1)
    glutSolidCube(1)

    glPopMatrix()

    glPushMatrix()
    glTranslatef(0.8, 0, 0.8)
    glRotatef(90, 1, 0, 0)
    glScalef(0.5, 0.5, 0.5)
    glutSolidTorus(0.2, 1, 10, 10)
    glPopMatrix()

    glPushMatrix()
    glTranslatef(1, 0, 1.5)
    glRotatef(90, 0, 1, 0)
    glScalef(1, 1, 4)
    glutSolidCube(1)
    glPopMatrix()

    glPushMatrix()
    glRotatef(8, 0, 1, 0)
    glScalef(1.1, 0.8, 3)
    glutSolidCube(1)
    glPopMatrix()

    glPopMatrix()

def main():
    # Initialization of PyGame modules
    pg.init()
    # Initialization of Glut library
    glutInit(sys.argv)
    # Setting up the viewport, camera, backgroud and display mode
    display = (800,600)
    pg.display.set_mode(display, DOUBLEBUF|OPENGL)
    glClearColor(0.1,0.1,0.1,0.3)
    gluPerspective(45, (display[0]/display[1]), 0.1, 50.0)
    gluLookAt(5,5,3,0,0,0,0,0,1)

    glTranslatef(0.0,0.0, -5)
    while True:
        # Listener for exit command
        for event in pg.event.get():
            if event.type == pg.QUIT:
                pg.quit()
                quit()

        # Clears the screen for the next frame to be drawn over
        glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
        ############## INSERT CODE FOR GENERATING OBJECTS ##################
        draw_gun()
        ####################################################################
        # Function used to advance to the next frame essentially
        pg.display.flip()
        pg.time.wait(10)

Todo este montón de código nos da:

plotted object

Conclusión

OpenGL es muy antiguo, y no encontrará muchos tutoriales en línea sobre cómo usarlo correctamente y entenderlo porque todos los perros principales ya están hasta las rodillas en nuevas tecnologías.

Para utilizar correctamente OpenGL, es necesario comprender los conceptos básicos para comprender las implementaciones a través de las funciones de OpenGL.

En este artículo, hemos cubierto operaciones básicas de matriz (traslación, rotación y escalado), así como transformaciones compuestas y transformaciones que involucran un punto de referencia.

En el próximo artículo, usaremos PyGame y [PyOpenGL] (http://pyopengl.sourceforge.net/) para inicializar un proyecto, dibujar objetos, animarlos, etc. arlos, etc.