Autor projekta:
fnovacki@foi.hr
filip@novacki.org
github.com/filipnovacki
Biblioteke:
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 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 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)
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
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
)
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]))
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)
(env) $ python simple_circle.py
U ovom primjeru postoji i bug koji nastaje u fullscreen prikazu
(env) $ python simple_circle_with_colours.py
(env) $ python cube_rotating.py
(env) $ python perspective.py
Literatura i korisne poveznice: