Введение в OpenGL и PyOpenGL. Часть I: создание вращающегося куба

Когда я впервые начал изучать OpenGL с Python, моей главной целью было выяснить, как создать вращающийся куб. Я не думаю, что я одинок, поскольку это, кажется, вершина понимания основ OpenGL. В итоге я смонтировал это первое видео, включив в него все: от установки Python, PyOpenGL и PyGame до написания необходимого кода для создания вращающегося куба. Эта первая статья получилась довольно длинной, но мне хотелось включить сюда все, что касается данной темы.

Разобраться в OpenGL мне было довольно трудно. В основном, потому что большинство руководств по OpenGL с PyOpenGL, которые я смог найти, были явно написаны для тех, кто уже знал OpenGL. Я OpenGL не знал, так что это было серьезным препятствием для меня. Надеюсь, я смогу помочь вам понять эту тему намного быстрее, чем удалось мне.

Во-первых, PyOpenGL — это всего лишь в некотором роде оболочка Python вокруг OpenGL. Она позволяет манипулировать OpenGL при помощи Python. OpenGL — это межъязыковой API, поэтому вы можете перенести свои знания OpenGL на другие языки.

Как же работает OpenGL? Вы просто определяете объекты в пространстве. Например, для куба вы указываете его углы. Углы называются вершинами или нодами.

Как только вы определите вершины, вы сможете что-то с ними делать. В этом примере мы хотим провести между ними линии. Определение вершин в Python выполняется с помощью простого списка или кортежа. Затем вы можете заранее задать некоторые правила. Например, можно указать, какие вершины составляют поверхность и между какими вершинами находятся ребра или линии, которые мы хотим провести между вершинами.

Сделав это, вы готовы начать писать код OpenGL. Этот код помещается между выражениями glBegin и glEnd. В выражении glBegin мы указываем тип кода, который мы собираемся передать. Обычно это константы, например GL_QUADS или GL_LINES. Они сообщают библиотеке OpenGL, каким образом она должна обрабатывать ваши выражения.

Итак, это была довольно общая концепция того, как работает OpenGL, давайте теперь продолжим и напишем код!

Для начала нам надо иметь вот что: Python, PyOpenGLPyGame.

Если вы пользователь Windows, я настоятельно рекомендую загрузить PyGame и PyOpenGL из этого источника: Windows binaries for Python Modules. Обязательно сохраните эту ссылку, это крайне полезный сайт.

Загрузив все это, откройте вашу IDE и выполните следующий код:

import pygame
import OpenGL

Если эти выражения не вызовут ошибок, значит вы готовы к дальнейшей работе. А если будут ошибки, значит что-то пошло не так. Как правило, ошибки возникают при несовпадении версий Python, PyGame или PyOpenGL. Также они могут возникнуть при несовпадении битовости Python и загружаемых в него библиотек. То есть, если вы используете 32-битный Python, вам нужно использовать 32-битные модули библиотек и так далее.

Даже если ваша операционная система — 64-битная, все равно может оказаться, что вы используете 32-битную версию Python. Мы настоятельно рекомендуем по возможности использовать 64-битный Python. Дело в том, что 32-битный Python может использовать только 2 ГБ оперативной памяти, а это существенное ограничение. Разумеется, если у вас 32-битная ОС, вы не можете использовать 64-битный Python.

Отлично, теперь перейдем непосредственно к коду! Если у вас еще сохранился код импорта, который мы только что запускали, сотрите его: мы начнем полностью с чистого листа.

Для начала произведем импорт необходимых библиотек:

import pygame
from pygame.locals import *
from OpenGL.GL import *
from OpenGL.GLU import *

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

Затем мы импортируем OpenGL.GL и OpenGL.GLU. OpenGL.GL содержит в себе самые обычные функции библиотеки OpenGL, а вот OpenGL.GLU — более забавные.

Теперь вспомним про наши вершины:

vertices= (
    (1, -1, -1),
    (1, 1, -1),
    (-1, 1, -1),
    (-1, -1, -1),
    (1, -1, 1),
    (1, 1, 1),
    (-1, -1, 1),
    (-1, 1, 1)
    )

Здесь мы определили координаты (x, y, z) каждой нашей вершины. Думаю, лучше всего представить это в относительных единицах. Попробуйте представить их в пространстве. Напоминаю, что у куба восемь вершин.

Далее нам надо задать ребра:

edges = (
    (0,1),
    (0,3),
    (0,4),
    (2,1),
    (2,3),
    (2,7),
    (6,3),
    (6,4),
    (6,7),
    (5,1),
    (5,4),
    (5,7)
    )

Каждое ребро представлено кортежем, состоящим из двух чисел. Эти числа соответствуют номерам вершин, а ребро их соединяет. Как принято в Python, да и во многих других языках программирования, нумерация начинается с 0. Соответственно, 0 обозначает вершину (1, -1, -1), и так далее.

Теперь, когда у нас все это есть, давайте поработаем над необходимым кодом для работы с OpenGL, чтобы фактически создать сам куб:

def Cube():
    glBegin(GL_LINES)
    for edge in edges:
        for vertex in edge:
            glVertex3fv(vertices[vertex])
    glEnd()

Как обычно, мы начинаем с задания функции. Поскольку это просто функция, которая будет содержать код OpenGL, мы начинаем этот код со следующего выражения: glBegin (GL_LINES). Это уведомляет OpenGL сначала о том, что мы собираемся бросить в нее какой-то код, а затем — о том, как надо обрабатывать этот код (это указывает аргумент GL_LINES). В данном случае этот код будет рассматриваться как код для рисования линий.

Далее, мы итерируем по всем нашим ребрам (список edges), а затем каждой вершине в ребре (их там две) мы ставим в соответствие вершину из нашего списка вершин vertices (при помощи функции glVertex3fv).

В конечном итоге в OpenGL передаются константы GL_LINES:

glVertex3fv((1, -1, -1))
glVertex3fv((1, 1, -1))

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

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

Это все, что касается нашей функции. Она создает куб, но теперь мы хотим его отобразить и указать его перспективу в нашем окружении:

def main():
    pygame.init()
    display = (800,600)
    pygame.display.set_mode(display, DOUBLEBUF|OPENGL)

Это тоже очень типичный для Pygame код. Единственное существенное отличие здесь в том, что после параметра display в функции pygame.display.set_mode мы добавляем еще один параметр. На самом деле это константы, уведомляющие PyGame о том, что мы будем использовать код OpenGL. Константа DOUBLEBUF расшифровывается как двойной буфер. Она обозначает тип буфферизации, в котором есть два буфера для соответствия кадровой частоте монитора. Обратите внимание, что для разделения констант используется символ «|». Мы еще столкнемся с ним в дальнейшем.

Идем дальше. В теле главной функции находится следующий код:

gluPerspective(45, (display[0]/display[1]), 0.1, 50.0)

Функция gluPerspective определяет перспективу, о чем, впрочем, несложно догадаться из ее названия. Ее первый параметр определяет угол поля зрения и выражается в градусах. Второй параметр — это соотношение сторон дисплея, ширина, деленная на высоту. Следующие два параметра — znear и zfar, которые представляют собой ближнюю и дальнюю плоскости отсечения.

Что, черт возьми, означает плоскость отсечения? Если вы похожи на меня, на данный момент это ничего вам не говорит. По сути, плоскость отсечения — это расстояние, на котором объект либо появляется, либо исчезает. Таким образом, объект будет виден только между этими двумя значениями, и оба значения должны быть положительными, потому что они связаны с вашей перспективой, а не с вашим фактическим местоположением в трехмерной среде.

Итак, у нас есть ближняя плоскость отсечения, находящаяся на расстоянии 0.1 единицы, и дальняя плоскость отсечения на расстоянии 50.0 единиц. Это станет более понятным несколько позже, как только мы отобразим куб и сможем контролировать свое местонахождение в 3D-среде. Именно тогда вы увидите плоскости отсечения в действии.

Идем дальше. У нас есть следующая функция:

glTranslatef(0.0,0.0, -5)

Функция glTranslatef, цитируя дословно, «умножает текущую матрицу на матрицу перехода». Круто, но для меня это опять ничего не значит. Итак, с точки зрения непрофессионала, это в основном перемещает вас, то есть сдвигает параметры ваших координат — x, y и z. Таким образом, вышеуказанный код означает, что мы сдвигаемся на 5 единиц назад. Это делается для того, чтобы мы действительно могли видеть куб, когда создаем его. Иначе мы были бы слишком близко.

Теперь напишем наш типичный цикл для отслеживания событий в PyGame. Опять же, если вы хотите узнать больше, ознакомьтесь со статьями, которые уже упоминались ранее.

while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            quit()

Это простой цикл отслеживания событий в PyGame, который определяет возможность выхода. Иными словами, он отслеживает нажатие клавиши "x". Далее, под оператором while продолжаем наш код:

    glRotatef(1, 3, 1, 1)
    glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
    Cube()
    pygame.display.flip()
    pygame.time.wait(10)

Функция glRotatef умножает текущую матрицу на матрицу вращения. Ее параметрами являются угол вращения и координаты x, y, и z.

Затем у нас есть функция glClear, работающая, как любая другая функция очистки. Мы указываем в ее параметрах пару констант, которые сообщают OpenGL, что именно мы очищаем.

Как только мы очистим «экран», мы опять вызовем нашу функцию Cube ().

После этого мы вызываем функцию pygame.display.flip (), которая обновляет наш экран.

И наконец, мы вставляем небольшую задержку при помощи функции pygame.time.wait (10).

Это все, что касается нашей главной функции main(), и теперь мы просто вызываем ее в конце, чтобы все заработало. На случай, если вы где-то заблудились, вот весь наш код, собранный воедино:

import pygame
from pygame.locals import *

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

verticies = (
    (1, -1, -1),
    (1, 1, -1),
    (-1, 1, -1),
    (-1, -1, -1),
    (1, -1, 1),
    (1, 1, 1),
    (-1, -1, 1),
    (-1, 1, 1)
    )

edges = (
    (0,1),
    (0,3),
    (0,4),
    (2,1),
    (2,3),
    (2,7),
    (6,3),
    (6,4),
    (6,7),
    (5,1),
    (5,4),
    (5,7)
    )


def Cube():
    glBegin(GL_LINES)
    for edge in edges:
        for vertex in edge:
            glVertex3fv(verticies[vertex])
    glEnd()


def main():
    pygame.init()
    display = (800,600)
    pygame.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 pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                quit()

        glRotatef(1, 3, 1, 1)
        glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
        Cube()
        pygame.display.flip()
        pygame.time.wait(10)


main() 

Результат его выполнения должен быть следующим:

вращающийся куб, Введение в OpenGL и PyOpenGL. Часть I: создание вращающегося куба

Отлично, поздравляем вас с созданием куба при помощи PyOpenGL! Очевидно, что вам еще многое предстоит узнать, и работа многих функций пока для вас до конца не понятна. Мы скоро рассмотрим их более подробно.

Следующая статья — Введение в OpenGL и PyOpenGL. Часть II: раскрашивание поверхностей вращающегося куба.