Рубрики
Без рубрики

Расширенный OpenGL в Python с PyGame и PyOpenGL

PyOpenGL-это стандартизированный мост между OpenGL и Python. PyGame-это стандартизированная библиотека для создания игр на Python. В этой статье мы будем использовать эти два подхода и рассмотрим некоторые важные темы в OpenGL с Python.

Автор оригинала: Vladimir Batoćanin.

Вступление

Следуя предыдущей статье, Понимание OpenGL через Python где мы заложили основу для дальнейшего обучения, мы можем перейти к OpenGL с помощью PyGame и PyOpenGL .

PyOpenGL-это стандартизированная библиотека, используемая в качестве моста между Python и API OpenGL, а PyGame-это стандартизированная библиотека, используемая для создания игр на Python. Он предлагает встроенные удобные графические и звуковые библиотеки, и мы будем использовать его для более легкого отображения результата в конце статьи.

Как упоминалось в предыдущей статье, OpenGL очень стар, поэтому вы не найдете много учебников в Интернете о том, как правильно его использовать и понимать, потому что все лучшие собаки уже по колено погружены в новые технологии.

В этой статье мы перейдем к нескольким фундаментальным темам, которые вам нужно знать:

  • Инициализация проекта с помощью PyGame
  • Рисование Объектов
  • Интерактивная анимация
  • Использование Матриц Преобразования
  • Многократное Выполнение Преобразований
  • Пример реализации

Инициализация проекта с помощью PyGame

Во-первых, нам нужно установить PyGame и PyOpenGL, если вы еще этого не сделали:

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

Примечание : Более подробную установку вы можете найти в предыдущей статье OpenGL.

Если у вас возникли проблемы с установкой, то вам стоит посетить раздел PyGame “Начало работы”|/.

Поскольку нет никакого смысла выгружать на вас 3 книги по теории графики, мы будем использовать библиотеку PyGame, чтобы дать нам фору. По сути, это просто сократит процесс от инициализации проекта до фактического моделирования и анимации.

Для начала нам нужно импортировать все необходимое как из OpenGL, так и из PyGame:

import pygame as pg
from pygame.locals import *

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

Далее мы переходим к инициализации:

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

Хотя инициализация состоит всего из трех строк кода, каждая из них заслуживает, по крайней мере, простого объяснения:

  • pg.init() : Инициализация всех модулей PyGame – эта функция просто находка
  • размер окна = (1920, 1080) : Определение фиксированного размера окна
  • pg.display.set_mode(display, DOUBLE BUF|OPENGL) : Здесь мы указываем, что будем использовать OpenGL с двойной буферизацией

Двойная буферизация означает, что в любой момент времени есть два изображения – одно, которое мы можем видеть, и другое, которое мы можем трансформировать по своему усмотрению. Мы видим фактическое изменение, вызванное преобразованиями, когда два буфера меняются местами.

Поскольку у нас есть настроенный видовой экран, далее нам нужно указать, что мы будем видеть, или, скорее, где будет размещена “камера”, и как далеко и широко она может видеть.

Это известно как усеченное – это просто отрезанная пирамида, которая визуально представляет зрение камеры (то, что она может и не может видеть).

A frustum определяется 4 ключевыми параметрами:

  1. FOV (Поле зрения) : Угол в градусах
  2. Соотношение сторон : Определяется как отношение ширины и высоты
  3. Координата z ближней плоскости отсечения : Минимальное расстояние рисования Координата z дальней плоскости отсечения
  4. : Максимальное расстояние рисования

Итак, давайте продолжим и реализуем камеру с учетом этих параметров, используя код OpenGL C:

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

Чтобы лучше понять, как работает усеченное тело, вот эталонная картинка:

вид усеченного конуса

Ближние и дальние самолеты используются для лучшей производительности. Реально, рендеринг чего-либо вне нашего поля зрения-это пустая трата аппаратной производительности, которая может быть использована для рендеринга того, что мы действительно видим.

Таким образом, все, что игрок не может видеть, неявно хранится в памяти, даже если это визуально не присутствует. Вот отличное видео о том, как выглядит рендеринг только внутри усеченного конуса.

Рисование Объектов

После этой установки, я полагаю, мы задаем себе тот же вопрос:

Что ж, все это прекрасно и денди, но как мне сделать Супер-Звездный Разрушитель?

Хорошо… с точками . Каждая модель в объекте OpenGL хранится как набор вершин и набор их отношений (какие вершины связаны). Так что теоретически, если бы вы знали положение каждой точки, которая используется для рисования Суперзвездного Разрушителя, вы вполне могли бы нарисовать его!

Существует несколько способов моделирования объектов в OpenGL:

  1. Рисование с использованием вершин, и в зависимости от того, как OpenGL интерпретирует эти вершины, мы можем рисовать с помощью:
    • точки : как в буквальных точках, которые никак не связаны
    • lines : каждая пара вершин строит связную линию
    • треугольники : каждые три вершины образуют треугольник
    • четырехугольник : каждые четыре вершины образуют четырехугольник
    • polygon : вы получаете точку
    • и многие другие…
  2. Рисование с использованием встроенных фигур и объектов, которые были тщательно смоделированы участниками OpenGL
  3. Импорт полностью смоделированных объектов

Итак, чтобы нарисовать куб, например, нам сначала нужно определить его вершины:

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))

Это довольно интуитивно понятно – суть 0 имеет преимущество с 1 , 3 , и 4 . В чем дело 1 имеет ребро с точками 3 , 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))

Это также интуитивно понятно – чтобы сделать четырехугольник на верхней стороне куба, мы хотели бы “раскрасить” все, что находится между точками 0 , 3 , 6 , и 4 .

Имейте в виду, что существует реальная причина, по которой мы помечаем вершины как индексы массива, в котором они определены. Это делает написание кода, который соединяет их очень легко.

Для рисования проводного куба используется следующая функция:

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

glBegin () – это функция, указывающая, что мы будем определять вершины примитива в приведенном ниже коде. Когда мы закончим определение примитива, мы используем функцию glEnd() .

GL_LINES – это макрос, который указывает, что мы будем рисовать линии.

glVertex3fv () – это функция, определяющая вершину в пространстве, существует несколько версий этой функции, поэтому для ясности давайте посмотрим, как строятся имена:

  • glVertex : функция, определяющая вершину
  • gl Vertex3 : функция, определяющая вершину с помощью 3 координат
  • glVertex3f : функция, определяющая вершину с использованием 3 координат типа GLfloat
  • glVertex3fv : функция, определяющая вершину с помощью 3 координат типа GLfloat , которые помещаются внутри вектора (кортежа) (альтернативой будет glVertex3fl , который использует список аргументов вместо вектора)

Следуя аналогичной логике, следующая функция используется для рисования сплошного куба:

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

Интерактивная анимация

Чтобы наша программа была “убиваемой” нам нужно вставить следующий фрагмент кода:

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

Это в основном просто слушатель, который прокручивает события Pygame, и если он обнаруживает, что мы нажали кнопку “убить окно”, он выходит из приложения.

Подробнее о событиях PyGame мы расскажем в следующей статье – эта статья была введена сразу же, потому что пользователям и вам самим было бы довольно неудобно запускать диспетчер задач каждый раз, когда они хотят выйти из приложения.

В этом примере мы будем использовать двойную буферизацию , что просто означает , что мы будем использовать два буфера (вы можете думать о них как о холстах для рисования), которые будут меняться местами через фиксированные интервалы и создавать иллюзию движения.

Зная это, наш код должен иметь следующий шаблон:

handleEvents()
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
doTransformationsAndDrawing()
pg.display.flip()
pg.time.wait(1)
  • glClear : функция, которая очищает указанные буферы (холсты), в данном случае color buffer (который содержит информацию о цвете для рисования сгенерированных объектов) и depth buffer (буфер, который хранит отношения in-front-of или in-back-of всех сгенерированных объектов).
  • pg.display.flip() : Функция, которая обновляет окно с активным содержимым буфера
  • pg.time.wait(1) : Функция, которая приостанавливает работу программы на определенный период времени.

glClear должен быть использован, потому что если мы его не используем, то будем просто закрашивать уже нарисованный холст, который в данном случае является нашим экраном, и мы закончим с беспорядком.

Далее, если мы хотим постоянно обновлять наш экран , точно так же, как анимацию, мы должны поместить весь наш код в цикл while , в котором мы:

  1. Обработка событий (в данном случае просто выход из системы)
  2. Очистите буферы цвета и глубины, чтобы их можно было снова нарисовать
  3. Преобразование и рисование объектов
  4. Обновите экран
  5. ГОТО 1.

Код должен выглядеть примерно так:

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

Использование Матриц Преобразования

В предыдущей статье мы объяснили, как теоретически нам нужно построить преобразование, имеющее реферальную точку.

OpenGL работает точно так же, как это видно из следующего кода:

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

В этом примере мы сделали z-ось вращение в xy-плоскости с центром вращения будучи (1,1,1) на 30 градусов.

Давайте немного освежимся, если эти термины звучат немного запутанно:

  1. ось z вращение означает, что мы вращаемся вокруг оси z

    Это просто означает, что мы аппроксимируем 2D-плоскость 3D-пространством, и все это преобразование в основном похоже на нормальное вращение вокруг реферальной точки в 2D-пространстве.

  2. Мы получаем xy-плоскость , сжимая все трехмерное пространство в плоскость, имеющую z=0 (мы всячески исключаем параметр z)
  3. Центр вращения-это вершина, вокруг которой мы будем вращать данный объект (по умолчанию центром вращения является исходная вершина (0,0,0) )

Но есть одна загвоздка – OpenGL понимает приведенный выше код, постоянно запоминая и модифицируя одну глобальную матрицу преобразования .

Поэтому, когда вы пишете что-то в OpenGL, вы говорите следующее:

# 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)

Как вы можете себе представить, это создает огромную проблему, потому что иногда мы хотим использовать преобразование для одного объекта, а не для всего исходного кода. Это очень распространенная причина ошибок в низкоуровневом OpenGL.

Чтобы бороться с этой проблемной особенностью OpenGL, мы представляем pushing and popping transformation matrix – glPushMatrix() и 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

Они работают по простому принципу Last-in-First-Out (LIFO). Когда мы хотим выполнить перевод в матрицу, мы сначала дублируем ее, а затем помещаем ее поверх стека матриц преобразования.

Другими словами, он изолирует все преобразования, которые мы выполняем в этом блоке, создавая локальную матрицу, которую мы можем отбросить после того, как закончим.

Как только объект переведен, мы выбрасываем матрицу преобразования из стека, оставляя остальные матрицы нетронутыми.

Многократное Выполнение Преобразований

В OpenGL, как уже упоминалось ранее, преобразования добавляются к активной матрице преобразования, которая находится поверх стека матриц преобразования.

Это означает, что преобразования выполняются в обратном порядке. Например:

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

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

В этом примере объект 1 сначала поворачивается, затем переводится, а объект 2 сначала переводится, а затем поворачивается. Последние два понятия не будут использоваться в примере реализации, но будут практически использованы в следующей статье серии.

Пример реализации

Приведенный ниже код рисует на экране сплошной куб и непрерывно поворачивает его на 1 градус вокруг вектора (1,1,1) . И его можно очень легко изменить, чтобы нарисовать проволочный куб, заменив cubeQuads ребрами куба.:

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()

Запустив этот фрагмент кода, появится окно PyGame, отображающее анимацию куба:

анимация кубика pygame

Вывод

Есть еще много чего узнать об OpenGL – освещение, текстуры, расширенное моделирование поверхности, композитная модульная анимация и многое другое.

Но не волнуйтесь, все это будет объяснено в следующих статьях, рассказывающих общественности об OpenGL должным образом, с нуля.

И не волнуйтесь, в следующей статье мы действительно нарисуем что-то полу-приличное.