PyOpenGL с шейдерами

  • Tutorial
image

В предыдущей статье были рассмотрены основы работы с OpenGL в Python. Для вывода графики использовались встроенные функции модуля glut и фиксированный конвейер OpenGL без шейдеров. По просьбе пользователей habrahabr.ru, на базе предыдущего урока был создан шаблон PyOpenGL приложения, использующего шейдеры и буферные объекты.
Роскошной графики, как и в предыдущей статье, ожидать не стоит. Цель данной статьи — продемонстрировать возможность работы с шейдерами и буферными объектами с использованием модуля PyOpenGL.

Итак, для работы нам понадобятся:
  • Интерпретатор языка Python (ссылка).
  • Среда разработки PyCharm (ссылка) (или любая другая на ваш вкус, подойдет даже блокнот).
  • Библиотека PyOpenGL (ссылка).

В среде разработке создадим и сохраним новый файл с кодом Python.
Для работы с 3D графикой (в частности, OpenGL) необходимо импортировать несколько модулей:
from OpenGL.GL import *
from OpenGL.GLUT import *

Модуль glut будет использован для создания окна и обработки нажатия клавиш. Дополнительно импортируем функцию random из одноименного модуля (пригодится для изменения цвета полигона):
from random import random

Подготовка.


Инициализируем режим отображения с использованием двойной буферизации и цветов в формате RGB (двойная буферизация позволяет избежать мерцания во время перерисовки экрана):
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB)

Зададим начальный размер окна (ширина, высота):
glutInitWindowSize(300, 300)

Укажем начальное положение окна относительно левого верхнего угла экрана:
glutInitWindowPosition(50, 50)

Выполним инициализацию OpenGl:
glutInit(sys.argv)

Создадим окно с заголовком «Shaders!»:
glutCreateWindow(b"Shaders!")

Определим процедуру, отвечающую за вывод графики на экран:
glutDisplayFunc(draw)

Определим процедуру, выполняющуюся при «простое» программы:
glutIdleFunc(draw)

Определяем процедуру, отвечающую за обработку специальных клавиш:
glutSpecialFunc(specialkeys)

Задаем серый цвет для очистки экрана:
glClearColor(0.2, 0.2, 0.2, 1)

До этого момента код был практически не отличим от использованного в предыдущей статье, но теперь все сильно меняется, в дело вступают шейдеры!
Для удобства создаем процедуру, подготавливающую шейдер к использованию:
# Процедура подготовки шейдера (тип шейдера, текст шейдера)
def create_shader(shader_type, source):
    # Создаем пустой объект шейдера
    shader = glCreateShader(shader_type)
    # Привязываем текст шейдера к пустому объекту шейдера
    glShaderSource(shader, source)
    # Компилируем шейдер
    glCompileShader(shader)
    # Возвращаем созданный шейдер
    return shader

С использованием процедуры create_shader создадим вершинный шейдер:
# Положение вершин не меняется
# Цвет вершины - такой же как и в массиве цветов
vertex = create_shader(GL_VERTEX_SHADER, """
varying vec4 vertex_color;
            void main(){
                gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
                vertex_color = gl_Color;
            }""")

Таким же образом создадим фрагментный шейдер:
# Определяет цвет каждого фрагмента как "смешанный" цвет его вершин
fragment = create_shader(GL_FRAGMENT_SHADER, """
varying vec4 vertex_color;
            void main() {
                gl_FragColor = vertex_color;
}""")

Создаем пустой объект шейдерной программы:
program = glCreateProgram()

Приcоединяем вершинный и фрагментный шейдеры к программе:
glAttachShader(program, vertex)
glAttachShader(program, fragment)

«Собираем» шейдерную программу:
glLinkProgram(program)

Сообщаем OpenGL о необходимости использовать данную шейдерную программу при выводе объектов на экран:
glUseProgram(program)

Теперь нам нужно определится что и каким цветом выводить на экран. Для этого создадим два массива. Первый массив — с координатами вершин (три вершины по три координаты):
pointdata = [[0, 0.5, 0], [-0.5, -0.5, 0], [0.5, -0.5, 0]]

Второй массив — с цветами для каждой вершины (по одному цвету для каждой):
pointcolor = [[1, 1, 0], [0, 1, 1], [1, 0, 1]]

Эти два массива можно объединить в один, но для наглядности и удобства восприятия они разнесены. На этом подготовительные мероприятия завершены и можно запустить основной цикл программы:
glutMainLoop()

Далее рассмотрим процедуры, отвечающие за обработку нажатий клавиш и, собственно, вывод объектов на экран.

Обработчик нажатий клавиш.


За обработку нажатий клавиш в нашей программе отвечает процедура specialkeys. В коде specialkeys в зависимости от того, какая стрелка на клавиатуре была нажата, мы, с использованием процедуры glRotatef, осуществляем поворот по осям x или y, по часовой стрелке или в обратном направлении, на 5 градусов. При нажатии клавиши END заполняем массив pointcolor случайными числами в диапазоне 0 до 1, тем самым меняя цвет отображаемого полигона. Код процедуры specialkeys:
# Процедура обработки специальных клавиш
def specialkeys(key, x, y):
    # Сообщаем о необходимости использовать глобального массива pointcolor
    global pointcolor
    # Обработчики специальных клавиш
    if key == GLUT_KEY_UP:          # Клавиша вверх
        glRotatef(5, 1, 0, 0)       # Вращаем на 5 градусов по оси X
    if key == GLUT_KEY_DOWN:        # Клавиша вниз
        glRotatef(-5, 1, 0, 0)      # Вращаем на -5 градусов по оси X
    if key == GLUT_KEY_LEFT:        # Клавиша влево
        glRotatef(5, 0, 1, 0)       # Вращаем на 5 градусов по оси Y
    if key == GLUT_KEY_RIGHT:       # Клавиша вправо
        glRotatef(-5, 0, 1, 0)      # Вращаем на -5 градусов по оси Y
    if key == GLUT_KEY_END:         # Клавиша END
        # Заполняем массив pointcolor случайными числами в диапазоне 0-1
        pointcolor = [[random(), random(), random()], [random(), random(), random()], [random(), random(), random()]]

Процедура перерисовки.


За перерисовку в нашей программе отвечает процедура draw. Первым делом очищаем экран и заполняем его серым цветом:
glClear(GL_COLOR_BUFFER_BIT)

Включаем использование массивов вершин и цветов:
glEnableClientState(GL_VERTEX_ARRAY)
glEnableClientState(GL_COLOR_ARRAY)

Далее укажем OpenGL где взять массив вершин, для этого используем процедуру glVertexPointer:
glVertexPointer(3, GL_FLOAT, 0, pointdata)

Первый параметр данной процедуры определяет сколько используется координат для одной вершины, второй параметр определяет тип данных для каждой координаты, третий параметр определяет смещение между вершинами в массиве. Если вершины идут одна за другой, то смещение 0. Четвертый параметр указывает на первую координату первой вершины в массиве.
Аналогично укажем OpenGL где взять массив цветов:
glColorPointer(3, GL_FLOAT, 0, pointcolor)

Все необходимые данные указаны, осталось только все нарисовать. С использованием процедуры glDrawArrays мы можем вывести все содержимое массивов на экран за один проход:
glDrawArrays(GL_TRIANGLES, 0, 3)

Первый параметр данной процедуры определяет то, какой тип примитивов будет использоваться при выводе объектов на экран (треугольники, точки, линии и др.), второй параметр должен указывать на начальный индекс в указанных массивах, третьим параметром мы указываем количество рисуемых примитивов (в нашем случае это 3 вершины — 9 координат).
Отключаем использование массивов вершин и цветов и выводим все нарисованное в памяти на экран:
glDisableClientState(GL_VERTEX_ARRAY) 
glDisableClientState(GL_COLOR_ARRAY) 
glutSwapBuffers()

В результате в окне программы мы наблюдаем треугольник с плавными переходами цветов. Треугольник можно вращать с использованием клавиш «стрелок». При нажатии кнопки END происходит смена цвета треугольника на случайный.

Весь код программы:
# -*- coding: utf-8 -*-
# Импортируем все необходимые библиотеки:
from OpenGL.GL import *
from OpenGL.GLUT import *
#import sys
# Из модуля random импортируем одноименную функцию random
from random import random
# объявляем массив pointcolor глобальным (будет доступен во всей программе)
global pointcolor


# Процедура обработки специальных клавиш
def specialkeys(key, x, y):
    # Сообщаем о необходимости использовать глобального массива pointcolor
    global pointcolor
    # Обработчики специальных клавиш
    if key == GLUT_KEY_UP:          # Клавиша вверх
        glRotatef(5, 1, 0, 0)       # Вращаем на 5 градусов по оси X
    if key == GLUT_KEY_DOWN:        # Клавиша вниз
        glRotatef(-5, 1, 0, 0)      # Вращаем на -5 градусов по оси X
    if key == GLUT_KEY_LEFT:        # Клавиша влево
        glRotatef(5, 0, 1, 0)       # Вращаем на 5 градусов по оси Y
    if key == GLUT_KEY_RIGHT:       # Клавиша вправо
        glRotatef(-5, 0, 1, 0)      # Вращаем на -5 градусов по оси Y
    if key == GLUT_KEY_END:         # Клавиша END
        # Заполняем массив pointcolor случайными числами в диапазоне 0-1
        pointcolor = [[random(), random(), random()], [random(), random(), random()], [random(), random(), random()]]


# Процедура подготовки шейдера (тип шейдера, текст шейдера)
def create_shader(shader_type, source):
    # Создаем пустой объект шейдера
    shader = glCreateShader(shader_type)
    # Привязываем текст шейдера к пустому объекту шейдера
    glShaderSource(shader, source)
    # Компилируем шейдер
    glCompileShader(shader)
    # Возвращаем созданный шейдер
    return shader


# Процедура перерисовки
def draw():
    glClear(GL_COLOR_BUFFER_BIT)                    # Очищаем экран и заливаем серым цветом
    glEnableClientState(GL_VERTEX_ARRAY)            # Включаем использование массива вершин
    glEnableClientState(GL_COLOR_ARRAY)             # Включаем использование массива цветов
    # Указываем, где взять массив верши:
    # Первый параметр - сколько используется координат на одну вершину
    # Второй параметр - определяем тип данных для каждой координаты вершины
    # Третий парметр - определяет смещение между вершинами в массиве
    # Если вершины идут одна за другой, то смещение 0
    # Четвертый параметр - указатель на первую координату первой вершины в массиве
    glVertexPointer(3, GL_FLOAT, 0, pointdata)
    # Указываем, где взять массив цветов:
    # Параметры аналогичны, но указывается массив цветов
    glColorPointer(3, GL_FLOAT, 0, pointcolor)
    # Рисуем данные массивов за один проход:
    # Первый параметр - какой тип примитивов использовать (треугольники, точки, линии и др.)
    # Второй параметр - начальный индекс в указанных массивах
    # Третий параметр - количество рисуемых объектов (в нашем случае это 3 вершины - 9 координат)
    glDrawArrays(GL_TRIANGLES, 0, 3)
    glDisableClientState(GL_VERTEX_ARRAY)           # Отключаем использование массива вершин
    glDisableClientState(GL_COLOR_ARRAY)            # Отключаем использование массива цветов
    glutSwapBuffers()                               # Выводим все нарисованное в памяти на экран


# Здесь начинется выполнение программы
# Использовать двойную буферезацию и цвета в формате RGB (Красный Синий Зеленый)
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB)
# Указываем начальный размер окна (ширина, высота)
glutInitWindowSize(300, 300)
# Указываем начальное
# положение окна относительно левого верхнего угла экрана
glutInitWindowPosition(50, 50)
# Инициализация OpenGl
glutInit(sys.argv)
# Создаем окно с заголовком "Shaders!"
glutCreateWindow(b"Shaders!")
# Определяем процедуру, отвечающую за перерисовку
glutDisplayFunc(draw)
# Определяем процедуру, выполняющуюся при "простое" программы
glutIdleFunc(draw)
# Определяем процедуру, отвечающую за обработку клавиш
glutSpecialFunc(specialkeys)
# Задаем серый цвет для очистки экрана
glClearColor(0.2, 0.2, 0.2, 1)
# Создаем вершинный шейдер:
# Положение вершин не меняется
# Цвет вершины - такой же как и в массиве цветов
vertex = create_shader(GL_VERTEX_SHADER, """
varying vec4 vertex_color;
            void main(){
                gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
                vertex_color = gl_Color;
            }""")
# Создаем фрагментный шейдер:
# Определяет цвет каждого фрагмента как "смешанный" цвет его вершин
fragment = create_shader(GL_FRAGMENT_SHADER, """
varying vec4 vertex_color;
            void main() {
                gl_FragColor = vertex_color;
}""")
# Создаем пустой объект шейдерной программы
program = glCreateProgram()
# Приcоединяем вершинный шейдер к программе
glAttachShader(program, vertex)
# Присоединяем фрагментный шейдер к программе
glAttachShader(program, fragment)
# "Собираем" шейдерную программу
glLinkProgram(program)
# Сообщаем OpenGL о необходимости использовать данную шейдерну программу при отрисовке объектов
glUseProgram(program)
# Определяем массив вершин (три вершины по три координаты)
pointdata = [[0, 0.5, 0], [-0.5, -0.5, 0], [0.5, -0.5, 0]]
# Определяем массив цветов (по одному цвету для каждой вершины)
pointcolor = [[1, 1, 0], [0, 1, 1], [1, 0, 1]]
# Запускаем основной цикл
glutMainLoop()


Результат выполнения программы (картинки):
image

image

image

и немного видео:
Стоит ли продолжать цикл статей о PyOpenGL?

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

Метки:
  • +28
  • 22,8k
  • 7
Поделиться публикацией
Комментарии 7
  • +1
    Оно конечно все хорошо и достаточно подробно, но вы бы лучше показывали какие-то вещи по-интереснее, а то выглядит все как helloworld.
    • 0
      Принято. Учту при написании следующего поста. Спасибо за комментарий.
      • 0
        что этот пример что прошлый — при тупом копировании «весь код программы» выдеёт:
        OpenGL.error.NullFunctionError: Attempt to call an undefined function glutInitDisplayMode, check for bool(glutInitDisplayMode) before calling
        • 0
          В примерах окно создается с использованием библиотеки freeglut, ошибка говорит о том, что не была найдена соответствующая библиотека freeglut.dll (ссылка). Поместите данную библиотеку в папку с интерпретатором Python или в папку с программой.
          • 0
            ничего не изменилось, ошибка таже… :(
            • +1
              Спасибо. Проблема была с моей стороны.Конкретно моей системе не хватало
              https://developer.nvidia.com/cg-toolkit-download

              Спасибо за поддержку.
            • +1
              Для Debian-based, если что apt install freeglut3 freeglut3-dev

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