Главная Статьи Ссылки Скачать Скриншоты Юмор Почитать Tools Проекты Обо мне Гостевая Форум |
Много лет я писал код под OpenGL исключительно на С++. Для удобства работ я использовал набор своих классов (таких как GlutWindow, Program, Texture и т.п.). На сайте есть старая статья про использование OpenGL из python, но она старая и давно устаревшая - там рассматривается OpenGL 2 и python 2.
При этом я часто использую python и он мне сильно нравится - он очень простой и удобный, крайне выразительный язык с огромным набором библиотек. Наличие встроенного мкенеджера пакетов pip сильно упрощает работу с многочисленными библиотеками и модулями (а также их зависимостями). Кроме того, многие небольшие примеры гораздо проще и быстрее написать на python, чем на С++. И довольно часто скорость приложения определяется скорее на стороне GPU, поэтому использование python не ведет к заметному замеделению программы, но упрощает установку и настройку многочисленных используемых библиотек.
Поэтому вполне естественно что в какой-то момент я решил посмотреть а что сеййчас в python с поддержкой соврменного OpenGL (3.х и выше). И был приятно удивлен, увидев что все просто отлично. Есть готовый модуль pyopengl, дающий непосредственно поддержку современного OpenGL. Кроме него я пордобрал для себя небольшой набор вспомгательный (и очень удобный) библиотек, заметно упрощающих работу с OpenGL из python.
Важной частью написания приложения, использующего OpenGL, является взаимодействие с оконной системой - создание и уничтожение окон, изменение их свойств, обработка событий (от мыши, клавиатуры и т.п.). Для этого я решил попробовать glfw - обертку для популярной библиотеки GLFW. Особенностью данной библиотеки является то, что все имена функций были заменены на python style, т.е. например вместо glfwCreateWindow нужно использовать glfw.create_window.
Есть хорошая обертка над библиотекой GLM для python - pyglmю Она позволяет легко работать со всеми типами данных из GLM (вектора, матрицы, кватрнионы). Обратите внимание, что при передаче этих типов данных в качестве uniform-переменных через pyopengl необходимо преобразовать их в tuple через метод to_tuple.
Для загрузки текстур из таких форматов как JPG, PNG, BMP, TGA и т.п. очень удобно использовать библиотеку PILLOW. Обратите внимание, что если мы хотим загрузить сжатую текстуру (например, формата DXTn), то здесь нам PILLOW уже не поможет - мы должны сделать это сами.
Также очень удобно использовать numpy для представления данных в виде массивов из элементов базовых типов в пкамяти для загрузки в память GPU.
Для С++ я обычно использую Assimp, но тут ситуация оказалась хуже - есть обертка, но она ориентирована на python 2 и требует наличия бинарных файлов библиотеки (например, assimp.dll). Поэтому я решил использовать свой парсер для формата obj и библиотеку pygltflib для загрузке данных в форматах GLTF/GLB.
Все приводимые ниже примеры были проверены на python 3.8 и выше, хотя, скорее всего, будут работать на любом python 3. Установить нужные библиотеки можно при помощи следующей командной строки (и для windows и Linux):
python -m pip install pyopengl glfw pyglm numpy pillow pygltflib
Ниже приводится простейший пример, создающий окно для вывода при помощи OpenGL.
import sys
import glfw
from OpenGL.GL import *
if not glfw.init():
print ( 'GLFW initialization error' )
sys.exit ( 1 )
glfw.window_hint ( glfw.CONTEXT_VERSION_MAJOR, 3 )
glfw.window_hint ( glfw.CONTEXT_VERSION_MINOR, 3 )
glfw.window_hint ( glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE )
glfw.window_hint ( glfw.OPENGL_FORWARD_COMPAT, glfw.TRUE )
glfw.window_hint ( glfw.RED_BITS, 8 )
glfw.window_hint ( glfw.GREEN_BITS, 8 )
glfw.window_hint ( glfw.BLUE_BITS, 8 )
glfw.window_hint ( glfw.ALPHA_BITS, 8 )
glfw.window_hint ( glfw.DEPTH_BITS, 24 )
window = glfw.create_window ( 800, 600, "Test pyOpenGL window", None, None )
glfw.make_context_current ( window )
glViewport ( 0, 0, 800, 600 )
def display ():
glClear ( GL_COLOR_BUFFER_BIT )
while not glfw.window_should_close ( window ):
glfw.poll_events ()
display ()
glfw.swap_buffers ( window )
glfw.terminate ()
Поскольку я привык заворачивать все в классы, то естественно, что и для python и данных библиотек я создал свой небольшой набор вспомогательных классов (pure python, cross-platform). Эти классы очень близки к соответствующим классам на С++ (основное отличие использование класса Window вместо GlutWindow). Ряд примеров из книги я переписал на python, но полного переписывания всех примеров не будет. Я использую свои функции для разбора DDS и загрузки текстур, но пока не все форматы поддерживаются, поддержка новых форматов будет постепенно добавляться.
Базовым классом будет класс Window, инкапсулирующий в себе поддержку окна, в которое будет проходить рендеринг. За собственно сам рендеринг и обработку сообщений оконной системы отвечают методы этого класса. Обратите внимание, что хотя названия методов соответствуют использованным в классе GlutWindow (C++), но входные аргументы слегка отличаются из-за специфики бииблиотеки GLFW. Ниже приводится простейшее приложение с использованием этого класса, полностью эквивалентное приведенному выше примеру.
# 1st example - create empty window
import Window
# create window with size and caption
w = Window.Window ( 700, 600, "Test window" )
# run it
w.run()
Для работы с шейдерами я использую класс Program. В отличии от своего аналога на C++ он использует именованные аргументы в конструкторе и может примать на вход как составной glsl-файл (как в примерах к книге), так и отдельные имена файлов (vertex="a.vsh" fragment = "a.fsh"). Кроме того он поддерживает #include внутри glsl-файлов.
Следующий пример выводит разноцветный треугольник. Данные в вершинах задаются через массивы и по ним строятся VAO и VBO.
import numpy
from OpenGL.GL import *
import Window
import Program
vertices = [
-1.0, -1.0, 0.0,
0.0, 1.0, 0.0,
1.0, -1.0, 0.0
]
class MyWindow ( Window.Window ):
def __init__ ( self, w, h, t ):
super().__init__ ( w, h, t, fullScreen = True )
self.vao = glGenVertexArrays(1)
self.vbo = glGenBuffers(1)
glBindVertexArray ( self.vao )
glBindBuffer ( GL_ARRAY_BUFFER, self.vbo )
glBufferData ( GL_ARRAY_BUFFER, 4 * len(vertices), numpy.array ( vertices, dtype = numpy.float32), GL_STATIC_DRAW )
# position attribute (loc=0, size=3, float, do not normalize, stride, offset)
glVertexAttribPointer ( 0, 3, GL_FLOAT, GL_FALSE, 4*3, ctypes.c_void_p(0) )
glEnableVertexAttribArray ( 0 )
self.shader = Program.Program ( vertex = "simple.vsh", fragment = "simple.fsh" )
self.shader.use ()
def redisplay ( self ):
glClearColor ( 0.2, 0.3, 0.2, 1.0 )
glClear ( GL_COLOR_BUFFER_BIT + GL_DEPTH_BUFFER_BIT )
glEnable ( GL_DEPTH_TEST )
glDrawArrays ( GL_TRIANGLES, 0, 6 )
def main():
win = MyWindow ( 900, 900, "Render triangle" )
win.run ()
if __name__ == "__main__":
main()
Для работы с буферами используется класс Buffer, внутри себя использующий numpy для перевода массивов в numpy.array с заданным типом для загрузки в память GPU.
# vb - list of data values
vel = Buffer.Buffer ( GL_SHADER_STORAGE_BUFFER, vb )
vel.bindBase ( GL_SHADER_STORAGE_BUFFER, 1 )
В репозитории с исходным кодом к книге () вы можете найти много примеров на python (в каталоге Code/python-code), ниже я расмотрю только несколько из них. Ниже приводится python-код для вывода объекта с использованием модели освещения Блинна-Фонга.
import math
import glm
from OpenGL.GL import *
import Window
import Program
import Mesh
class MyWindow ( Window.RotationWindow ):
def __init__ ( self, w, h, t ):
super().__init__ ( w, h, t )
self.eye = glm.vec3 ( -7, 0, 0 )
self.lightDir = glm.vec3 ( -1, 1, 1 )
self.kd = 0.8
self.ka = 0.2
self.ks = 0.2
self.mesh = Mesh.Mesh.createKnot ( 1, 1, 120, 30 )
self.shader = Program.Program ( glsl = "blinn-phong.glsl" )
self.shader.use ()
def redisplay ( self ):
glClearColor ( 0.2, 0.3, 0.2, 1.0 )
glClear ( GL_COLOR_BUFFER_BIT + GL_DEPTH_BUFFER_BIT )
glEnable ( GL_DEPTH_TEST )
self.shader.setUniformMat ( "mv", self.getRotation () )
self.shader.setUniformMat ( "nm", self.normalMatrix ( self.getRotation () ) )
self.mesh.render ()
def reshape ( self, width, height ):
super().reshape ( width, height )
self.shader.setUniformMat ( "proj", self.getProjection () )
self.shader.setUniformVec ( "eye", self.eye )
self.shader.setUniformVec ( "lightDir", self.lightDir )
self.shader.setUniformFloat ( "kd", self.kd )
self.shader.setUniformFloat ( "ks", self.ks )
self.shader.setUniformFloat ( "ka", self.ka )
def mouseScroll ( self, dx, dy ):
self.eye += glm.vec3 ( 0.1 * ( 1 if dy >= 0 else -1 ) )
self.reshape ( self.width, self.height )
def main():
win = MyWindow ( 900, 900, "Blinn-Phong shading model" )
win.run ()
if __name__ == "__main__":
main ()
Следующий пример использует рендеринг в текстуру и потом накладывает текстуру на выводимый объект. Это полный аналог примера из книги. Для вывода в текстуру используется класс Framebuffer.
import math
import glm
import numpy
from OpenGL.GL import *
import Window
import Program
import Texture
import Mesh
import Framebuffer
import dds
class MyWindow ( Window.RotationWindow ):
def __init__ ( self, w, h, t ):
super().__init__ ( w, h, t )
self.fb = Framebuffer.Framebuffer ( 512, 512, depth = True )
self.fb.bind ()
self.fb.attachTexture ( self.fb.createTexture () )
self.fb.unbind ()
self.mesh = Mesh.Mesh.createKnot ( 1, 4, 120, 30 )
self.cube = Mesh.Mesh.createBox ( glm.vec3 ( -1, -1, -1 ), glm.vec3 ( 2, 2, 2 ) )
self.texture = Texture.Texture ( "../../Textures/Fieldstone.dds" )
self.shader = Program.Program ( glsl = "rotate-8.glsl" )
self.shader.use ()
self.shader.setTexture ( "image", 0 )
self.eye = glm.vec3 ( 2.5, 2.5, 2.5 )
def redisplay ( self ):
self.displayBoxes ()
glClearColor ( 0.5, 0.5, 0.5, 1.0 )
glClear ( GL_COLOR_BUFFER_BIT + GL_DEPTH_BUFFER_BIT )
glEnable ( GL_DEPTH_TEST )
self.shader.use ()
self.fb.colorBuffers [0].bind ()
self.shader.setUniformMat ( "mv", self.getRotation () )
self.shader.setUniformMat ( "nm", self.normalMatrix ( self.getRotation () ) )
self.shader.setUniformVec ( "eye", self.eye )
self.cube.render ()
self.fb.colorBuffers [0].unbind ()
def reshape ( self, width, height ):
super().reshape ( width, height )
self.shader.setUniformMat ( "proj", self.getProjection () )
def mouseScroll ( self, dx, dy ):
self.eye += glm.vec3 ( 0.1 * ( 1 if dy >= 0 else -1 ) )
self.reshape ( self.width, self.height )
def displayBoxes ( self ):
self.fb.bind ()
self.texture.bind ()
self.shader.setUniformMat ( "mv", glm.rotate(glm.mat4(1), self.time (), glm.vec3(0, 0, 1)))
self.shader.setUniformVec ( "eye", glm.vec3 ( 7, 7, 7 ) )
glClearColor ( 0, 0, 0, 1 )
glClear ( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT )
self.mesh.render ()
self.texture.unbind ()
self.shader.unbind ()
self.fb.unbind ()
def main():
win = MyWindow ( 900, 900, "Environmapped knot" )
win.run ()
if __name__ == "__main__":
main()
from OpenGL.GL import *
import glm
import Window
import Program
import Buffer
class MyWindow ( Window.RotationWindow ):
def __init__ ( self, w, h, t ):
super().__init__ ( w, h, t )
self.vel = None
self.pos = None
self.compute = Program.Program ( glsl = "particles-compute.glsl" )
self.render = Program.Program ( glsl = "particles-render.glsl" )
self.initParticles ( 100 )
def redisplay ( self ):
glClearColor ( 0.2, 0.3, 0.2, 1.0 )
glClear ( GL_COLOR_BUFFER_BIT + GL_DEPTH_BUFFER_BIT )
glEnable ( GL_DEPTH_TEST )
self.compute.bind ()
self.compute.setUniformFloat ( "deltaT", 0.01 )
glDispatchCompute ( self.numParticles // 1000, 1, 1 )
glMemoryBarrier ( GL_SHADER_STORAGE_BARRIER_BIT )
self.render.use ()
self.render.setUniformMat ( "mvp", self.getProjection () * self.getRotation () )
glBindVertexArray ( self.vao )
self.pos.bind ( GL_ARRAY_BUFFER )
glPointSize ( 1.0 )
glDrawArrays ( GL_POINTS, 0, self.numParticles )
def mouseScroll ( self, dx, dy ):
self.eye += glm.vec3 ( 0.5 * ( 1 if dy >= 0 else -1 ) )
self.reshape ( self.width, self.height )
def initParticles ( self, num ):
n = num
self.numParticles = n * n * n
self.eye = glm.vec3 ( -0.5, 0.5, 25 )
# init buffers with particle data
vb = []
pb = []
h = 2.0 / (n - 1)
for i in range ( n ):
for j in range ( n ):
for k in range ( n ):
pb.append ( h * i - 1 )
pb.append ( h * j - 1 )
pb.append ( h * k - 1 )
pb.append ( 1 )
vb.append ( 0 )
vb.append ( 0 )
vb.append ( 0 )
vb.append ( 0 )
# create VBO'a and VAO
self.vel = Buffer.Buffer ( GL_SHADER_STORAGE_BUFFER, vb )
self.vel.bindBase ( GL_SHADER_STORAGE_BUFFER, 1 )
self.pos = Buffer.Buffer ( GL_SHADER_STORAGE_BUFFER, pb )
self.pos.bindBase ( GL_SHADER_STORAGE_BUFFER, 0 )
# create and init VAO
self.vao = glGenVertexArrays ( 1 )
glBindVertexArray ( self.vao )
self.pos.bind ( GL_ARRAY_BUFFER )
self.render.use ()
glVertexAttribPointer ( 0, 4, GL_FLOAT, GL_FALSE, 16, ctypes.c_void_p(0))
glEnableVertexAttribArray ( 0 )
#self.render.setAttrPtr ( "pos", 4, 4*4, ctypes.c_void_p(0) )
self.render.unbind ()
glBindVertexArray ( 0 )
# setup blending
glEnable ( GL_BLEND )
glBlendFunc ( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA )
def main():
win = MyWindow ( 900, 900, "Compute particles" )
win.run ()
if __name__ == "__main__":
main()
В ряде классов (Program, Framebuffer) добавлена поддержка оператора with, позволяющего автоматически вызывать bind/unbind как показано на примере ниже:
with self.framebuffer:
with self.shader:
self.mesh.render ()
Все рассмотренные примеры и классы можнонайти на в репозитории к книге на github в каталоге Code/python-code.