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

2 min


Введение

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

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

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

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

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

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

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

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

Если у вас есть проблемы, касающиеся установки, PyGame's "Начиная" раздел может быть хорошим местом для посещения.

Поскольку нет смысла выкладывать на вас теорию графики на 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 – эта функция – находка
  • windowSize = (1920, 1080): Определение фиксированного размера окна
  • pg.display.set_mode(display, DOUBLEBUF|OPENGL): Здесь мы указываем, что мы будем использовать OpenGL с двойная буферизация

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

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

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

усеченный определяется 4 ключевыми параметрами:

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

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

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

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

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

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

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

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

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

Ну, все в порядке, но как мне сделать суперзвездный разрушитель?

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

Есть несколько способов, которыми мы можем моделировать объекты в OpenGL:

  1. Рисование с использованием вершин, и в зависимости от того, как OpenGL интерпретирует эти вершины, мы можем рисовать с помощью:
    • точки: как в буквальных точках, которые никак не связаны
    • линии: каждая пара вершин строит связную линию
    • треугольники: каждые три вершины образуют треугольник
    • четырехугольник: каждые четыре вершины образуют четырехугольник
    • многоугольник: вы получаете точку
    • многое другое…
  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: функция, которая определяет вершину
  • glVertex3: функция, которая определяет вершину, используя 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()

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

Для нашей программы "Killable" нам нужно вставить следующий фрагмент кода:

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: Функция, которая очищает указанные буферы (холсты), в данном случае цветовой буфер (которая содержит информацию о цвете для рисования сгенерированных объектов) и буфер глубины (буфер, в котором хранятся отношения перед или внутри всех сгенерированных объектов).
  • pg.display.flip(): Функция, обновляющая окно с активным содержимым буфера
  • pg.time.wait(1): Функция, которая приостанавливает программу на определенный период времени

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

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

  1. Обработка событий (в данном случае просто выход)
  2. Очистите буферы цвета и глубины, чтобы их можно было снова рисовать
  3. Преобразуйте и рисуйте объекты
  4. Обновить экран
  5. GOTO 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-ось вращение в ху-плоскость с центр вращения существо (1,1,1) на 30 градусов.

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

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

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

  2. Мы получаем ху-плоскость раздавливая все трехмерное пространство в плоскость, которая имеет 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 мы представляем толкая а также выскакивают матрицы преобразования – 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

Они работают в простом Лифо (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()
#################################

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

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

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

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, отображающее анимацию куба:

Заключение

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

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

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


0 Comments

Ваш e-mail не будет опубликован. Обязательные поля помечены *