Python i OpenGL

Autor projekta:

  • Filip Novački
  • fnovacki@foi.hr
  • filip@novacki.org
  • github.com/filipnovacki

Biblioteke:

  • OpenGL
  • numpy
  • pyrr
  • glfw

U ovoj projektnoj dokumentaciji neće biti opisano toliko specifičnost OpenGL-a, već će naglasak biti stavljen na Python i na to kako su pojedini problemi riješeni.

Skripte korištene u projektu: https://github.com/filipnovacki/fictional-lamp

Instalacija svih paketa trebala bi funkcionirati pokretanjem naredbi u terminalu:

$ python -m venv env
$ source env/bin/activate
(env) $ pip install -r requirements.txt

Kod rada sa shaderima bilo je problema vezano za verzije OpenGL-a tako da nije nemoguće da i tamo može zapesti.

Verzije OpenGL-a:

$ glxinfo | grep "OpenGL"
OpenGL ES profile version string: OpenGL ES 3.0 Mesa 20.3.2
OpenGL ES profile shading language version string: OpenGL ES GLSL ES 3.00

GLFW

GLFW je biblioteka za stvaranje prozora. U Pythonu radi jednako kao i u sklopu OpenGL-a, jedina je razlika u tome što je nazivlje malo promijenjeno tako da je prilagođeno Pythonu. Tako se koriste rijeci_s_donjom_crticom umjesto camelCase sintakse.

screen_width = 1366
screen_heigh = 768
window = glfw.create_window(screen_width, screen_height, "PyOpenGL", None, None)
# (...)
if not window:
    glfw.terminate()

glfw.make_context_current(window)

# (...)
# shaders, binding...
# (...)

while not glfw.window_should_close(window):
    glfw.poll_events()
    # render, draw...
    glfw.swap_buffers(window)

Shaderi

Shaderi rade na vrlo sličan način kao i u drugim programskim jezicima za OpenGL. U Pythonu je možda malo kraći put za kompajliranje jer se shaderi u program mogu učinkovito pretvoriti u samo jednoj liniji koda.

FRAGMENT_SHADER = """
// ...
"""

VERTEX_SHADER = """
// ...
"""

shader = OpenGL.GL.shaders.compileProgram(
            OpenGL.GL.shaders.compileShader(VERTEX_SHADER, GL_VERTEX_SHADER),
            OpenGL.GL.shaders.compileShader(FRAGMENT_SHADER, GL_FRAGMENT_SHADER)
         )
# binding
VBO = glGenBuffers(1)
glBindBuffer(GL_ARRAY_BUFFER, VBO)
glBufferData(
    GL_ARRAY_BUFFER,
    circle.itemsize * len(circle),  # numpy array
    circle,                         # numpy array
    GL_STATIC_DRAW
)

position = glGetAttribLocation(shader, 'position')
glVertexAttribPointer(position, 2, GL_FLOAT, GL_FALSE, 0, None)
glEnableVertexAttribArray(position)

glUseProgram(shader)
glClearColor(1.0, 1.0, 1.0, 0.0)

Numpy i generiranje točaka

Kod crtanja potrebno je definirati točke u kojima će se nalaziti vrhovi te između kojih će se povući bridovi. Jedan od načina da se to napravi je ručno popisivati, odnosno

triangle = [
     0,     0.5,
    -0.5,  -0.5,
     0.5,  -0.5
]

Jasno, kad koristimo računala, takav pristup je spor i neučinkovit. Iz tog je razloga stvoreno nekoliko funkcija, odnosno modul objects. On služi za automatizirano stvaranje objekata koji se trebaju iscrtati.

Glavni alati koji se koriste kod generiranja točaka su funkcije iz paketa itertools koji dolazi s Pythonom.

Ovako je definirana funkcija za crtanje kvadra:

def draw_cube(side_a=0.5, side_b=0.6, side_c=0.7, colors=False):
    if not colors:
        return_vertices = np.array(list(chain(*product([1, -1], repeat=3))), dtype=np.float32)
    else:
        return_vertices = np.array([], dtype=np.float32)
        for point in np.array(list((product([1, -1], repeat=3)))) * list((repeat([side_a, side_b, side_c], times=8))):
            return_vertices = np.append(return_vertices, np.append(point, [np.random.random(), 0.299, 0.499]))
    return return_vertices

Cijeli ovaj kod radi na taj način da funkcija product generira sve kombinacije zadanih brojeva, odnosno Kartezijev produkt brojeva -1 i 1. Ukoliko je potreban dvodimenzionalan objekt, daje se argument repeat=2, a ako je potreban trodimenzionalni, daje se argument repeat=3.

Biblioteka Numpy ima izvrsno uređeno mmnoženje i zbrajanje svih vrsta objekata. Zbog načina na koji Numpy zna množiti arraye, oblik u kojem smo definirali kvadar pomoću -1 i 1 je odličan jer možemo jednostavno koordinate vrhova kvadra pomnožiti dužinama njihovih stranica kako bismo dobili kvadar s različitim veličinama stranica.

Među tim funkcijama korištena je i funkcija chain koja vraća elemente lista raspakirane u jednu listu, odnosno spojene. Takav zapis točaka je praktičan kod iscrtavanja.

Ostale funkcije za generiranje točaka:

def draw_circle(segments=10, r=0.5, colors=False):
    segment_len = np.math.tau / segments
    array = np.array([], dtype=np.float32)

    if not colors:
        array = np.append(array, [0.0, 0.0])
    else:
        array = np.append(array, [0.0, 0.0, 0.0, 0.0, 0.5])
    for x in range(segments + 1):
        x_coord = np.cos(x * segment_len)
        y_coord = np.sin(x * segment_len)
        if colors:
            row = [r * x_coord, r * y_coord, x_coord, y_coord, 0.5]
        else:
            row = [r * x_coord, r * y_coord]

        array = np.append(array, row)
    return array

Svojstva Numpy arraya

Numpy array je vrlo praktična struktura i zbog načina na koji se učitavaju točke u buffer.

U ovom isječku koda iskorišten je numpy array spremljen u varijabli circle. U tom tipu podataka spremljena su mnoga svojstva, između kojih i itemsize koji govori koliko je velik tip podataka spremljen u array. Kad se pomnoži s brojem elemenata strukture (len()), dobije se točno veličina objekta koja se treba pospremiti u buffer. Primjer:

VBO = glGenBuffers(1)
glBindBuffer(GL_ARRAY_BUFFER, VBO)
glBufferData(
    GL_ARRAY_BUFFER,
    circle.itemsize * len(circle),
    circle,
    GL_STATIC_DRAW
)

Matrice transformacije i pyrr

Za transformacije u prostoru koristi se biblioteka pyrr. Ona na jednostavan pruža operacije nad matricama, pa tako i transformacije po osima, od kojih su nama najzanimljivije transformacije po x i po y osima.

rot_x = pyrr.Matrix44.from_x_rotation(0.2 * glfw.get_time())
    rot_y = pyrr.Matrix44.from_y_rotation(0.3 * glfw.get_time())

    transformLoc = glGetUniformLocation(shader, "transform")
    glUniformMatrix4fv(transformLoc, 1, GL_FALSE, rot_x * rot_y)

Argument u metodi from_x_rotation() i from_y_rotation() iskazan je u radijanima. glfw mjeri vrijeme od kad je program pokrenut pa se rotacija može na jednostavan način dobiti rotacija davanjem vremena kao argumenta. Ovdje se vrijeme množi brojem manjim od 1 kako bi se rotacija usporila.

Kod perspektivne projekcije pyrr se koristi za stvaranje matrica transformacija:

view = pyrr.matrix44.create_from_translation(pyrr.Vector3([0.0, 0.0, -3.0]))
projection = pyrr.matrix44.create_perspective_projection(45.0, _SCREEN_WIDTH / _SCREEN_HEIGHT, 0.1, 100.0)
model = pyrr.matrix44.create_from_translation(pyrr.Vector3([0.0, 0.0, 0.0]))

Ostale Python specifičnosti u sinergiji s OpenGL-om

Kod povezivanja Pythona s OpenGL-om korisna je i Pythonova biblioteka ctypes. Ona inače služi za pozivanje vanjskih funkcija (ponajviše iz C-a), a ovdje možemo koristiti za određivanje veličine tipova. Tako kod povezivanja varijabli iz shadera moramo unijeti "pomak" u strukturi što se može vrlo lako napraviti uz asocijaciju nekih od ctypes i njihovim brojem. Primjer:

color = glGetAttribLocation(shader, 'color')
glVertexAttribPointer(
    color, 3, GL_FLOAT, GL_FALSE, 24, ctypes.c_void_p(12)
)
glEnableVertexAttribArray(color)

Rezultati projekta

Jednostavan krug

(env) $ python simple_circle.py

Jednobojan krug

Obojani krug

U ovom primjeru postoji i bug koji nastaje u fullscreen prikazu

(env) $ python simple_circle_with_colours.py

Jednostavan krug obojan

Jednostavan krug obojan

Ortogonalna projekcija

(env) $ python cube_rotating.py

Cube rotating

Perspektivna projekcija

(env) $ python perspective.py

In [ ]: