Список работ

OpenGL средствами pyglet

Содержание

Введение

Приводятся некоторые средства библиотеки pyglet для работы с OpenGL. Программирование выполняется на Python 3.6.6. Рассматривается вывод примитивов, заливка полигона с использованием растрового образа, применение материалов, наложение текстуры и употребление шейдеров.
Предполагается, что читатель знаком с основами компьютерной графики и OpenGL. По этой причине опускается рассмотрение вопросов проецирования, аффинных преобразований координат, систем цветов и пр., так же как и процедур и функций OpenGL.
Некоторые сведения об OpenGL можно найти, например, в [1-9].

Вывод прямоугольника и треугольников

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

import pyglet
from pyglet import app, graphics
from pyglet.window import Window
dx, dy = 25, 15 # Размеры прямоугольника
dx2 = dx / 2
dy2 = dy / 2
wx, wy = 1.5 * dx2, 1.5 * dy2 # Параметры области визуализации
width, height = int(20 * wx), int(20 * wy) # Размеры окна вывода
# Создаем окно визуализации
window = Window(visible = True, width = width, height = height,
                resizable = True, caption = 'Прямоугольник')
gl.glClearColor(0.1, 0.1, 0.1, 1.0) # Задаем почти черный цвет фона
gl.glClear(gl.GL_COLOR_BUFFER_BIT) # Заливка окна цветом фона
@window.event
def on_draw():
    # Проецирование
    gl.glMatrixMode(gl.GL_PROJECTION) # Теперь текущей является матрица проецирования
    gl.glLoadIdentity() # Инициализация матрицы проецирования
    gl.glOrtho(-wx, wx, -wy, wy, -1, 1) # Ортографическое проецирование
    gl.glColor3f(0, 1, 0) # Зеленый цвет
    gl.glBegin(gl.GL_QUADS) # Обход против часовой стрелки
    gl.glVertex3f(-dx2, -dy2, 0)
    gl.glVertex3f(dx2, -dy2, 0)
    gl.glVertex3f(dx2, dy2, 0)
    gl.glVertex3f(-dx2, dy2, 0)
    gl.glEnd()
app.run()

Результат показан на рис. 1.

Прямоугольник

Рис. 1. Прямоугольник

Тот же результат получим, применив graphics.draw [10] (приводится только процедура on_draw).

@window.event
def on_draw():
    gl.glMatrixMode(gl.GL_PROJECTION)
    gl.glLoadIdentity()
    gl.glOrtho(-wx, wx, -wy, wy, -1, 1)
    graphics.draw(4, gl.GL_QUADS,
                  ('v2f', (-dx2, -dy2, dx2, -dy2, dx2, dy2, -dx2, dy2)),
                  ('c3f', (0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0)))

Первый параметр – это число вершин, далее следуют кортежи с координатами вершин и их цветом.
Цвет можно задать перед graphics.draw:

    gl.glColor3f(0, 1, 0)
    graphics.draw(4, gl.GL_QUADS,
                 ('v2f', (-dx2, -dy2, dx2, -dy2, dx2, dy2, -dx2, dy2)))

Так же можно употребить graphics.vertex_list:

@window.event
def on_draw():
    gl.glMatrixMode(gl.GL_PROJECTION)
    gl.glLoadIdentity()
    gl.glOrtho(-wx, wx, -wy, wy, -1, 1)
    v_lst = graphics.vertex_list(4,
                                 ('v2f', (-dx2, -dy2, dx2, -dy2, dx2, dy2, -dx2, dy2)),
                                 ('c3f', (0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0)))
    v_lst.draw(gl.GL_QUADS)

При выводе нескольких примитивов, например треугольников, с общими вершинами более компактным будет применение graphics.draw_indexed, например:

@window.event
def on_draw():
    # Проецирование
    gl.glMatrixMode(gl.GL_PROJECTION)
    gl.glLoadIdentity()
    gl.glOrtho(-wx, wx, -wy, wy, -1, 1)
    graphics.draw_indexed(4, gl.GL_TRIANGLES,
                          [0, 1, 2, 1, 3, 2],
                          ('v2f', (-dx2, -dy2, 0, -dy / 4, 0, dy2, dx2, -dy2)),
                          ('c3f', (1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0)))

Всего для вывода двух смежных треугольников (рис. 2 ) достаточно задать 4 вершины.

Треугольники

Рис. 2. Треугольники

Чтобы отключить интерполяцию цветов, перед graphics.draw_indexed следует задать

gl.glShadeModel(gl.GL_FLAT) # По умолчанию употребляется GL_SMOOTH

Будет использован цвет вершины с индексом 2 – (0, 1, 0).

Вывод точек

Для точки можно задать ее размер:

gl.glPointSize(16)

и режим сглаживания:

gl.glEnable(gl.GL_POINT_SMOOTH)

Отобразим 8 точек: 4, используя glBegin / glEnd, и 4 сглаженные, применив draw, (рис. 3):

import pyglet
from pyglet import app, gl, graphics
from pyglet.window import Window
d, d2 = 12, 10
wx, wy = 1.5 * d, 1.2 * d # Параметры области визуализации
width, height = int(20 * wx), int(20 * wy) # Размеры окна вывода
window = Window(visible = True, width = width, height = height,
                resizable = True, caption = 'Точки')
gl.glClearColor(0.1, 0.1, 0.1, 1.0)
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
@window.event
def on_draw():
    gl.glMatrixMode(gl.GL_PROJECTION)
    gl.glLoadIdentity()
    gl.glOrtho(-wx, wx, -wy, wy, -1, 1)
    gl.glPointSize(16)
    gl.glBegin(gl.GL_POINTS)
    gl.glColor3f(1, 0, 0)
    gl.glVertex3f(-d, -d, 0)
    gl.glColor3f(0, 1, 0)
    gl.glVertex3f(-d, d, 0)
    gl.glColor3f(0, 0, 1)
    gl.glVertex3f(d, d, 0)
    gl.glColor3f(1, 1, 0)
    gl.glVertex3f(d, -d, 0)
    gl.glEnd()
    gl.glEnable(gl.GL_POINT_SMOOTH)
    graphics.draw(4, gl.GL_POINTS,
                  ('v2f', (-d2, -d2, -d2, d2, d2, d2, d2, -d2)),
                  ('c3B', (0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 0)))
    gl.glDisable(gl.GL_POINT_SMOOTH)
app.run()

Точки

Рис. 3. Точки

Вывод линий

Для линии можно задать ее толщину:

gl.glLineWidth(4)

и образец (маску):

gl.glEnable(gl.GL_LINE_STIPPLE)
pattern = '0b1111100110011111' # '1111100110011111'
gl.glLineStipple(2, int(pattern, 2)) # Повторяем каждый бит образца 2 раза

Линии (рис. 4) выводятся после задания GL_LINES, или GL_LINE_STRIP, или GL_LINE_LOOP:

import pyglet
from pyglet import app, gl, graphics
from pyglet.window import Window
d, d2, d3 = 12, 8, 6
wx, wy = 1.5 * d, 1.2 * d # Параметры области визуализации
width, height = int(20 * wx), int(20 * wy) # Размеры окна вывода
window = Window(visible = True, width = width, height = height,
                resizable = True, caption = 'Линии')
gl.glClearColor(0.1, 0.1, 0.1, 1.0)
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
@window.event
def on_draw():
    gl.glMatrixMode(gl.GL_PROJECTION)
    gl.glLoadIdentity()
    gl.glOrtho(-wx, wx, -wy, wy, -1, 1)
    gl.glLineWidth(4)
    gl.glBegin(gl.GL_LINES)
    gl.glColor3f(1, 0, 0)
    gl.glVertex3f(-d, -d, 0)
    gl.glVertex3f(-d, d, 0)
    gl.glColor3f(0, 1, 0)
    gl.glVertex3f(-d, d, 0)
    gl.glVertex3f(d, d, 0)
    gl.glColor3f(0, 0, 1)
    gl.glVertex3f(d, d, 0)
    gl.glVertex3f(d, -d, 0)
    gl.glColor3f(1, 1, 0)
    gl.glVertex3f(d, -d, 0)
    gl.glVertex3f(-d, -d, 0)
    gl.glEnd()
    graphics.draw(5, gl.GL_LINE_STRIP,
                  ('v2f', (-d, -d, -d, d, d, d, d, -d, -d, -d)),
                  ('c3f', (1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0)))
    gl.glEnable(gl.GL_LINE_STIPPLE)
    pattern = '0b1111100110011111' # '1111100110011111'
    gl.glLineStipple(2, int(pattern, 2)) # Повторяем каждый бит образца 2 раза
    gl.glColor3f(1, 1, 1)
    gl.glBegin(gl.GL_LINE_STRIP)
    gl.glVertex3f(-d2, -d2, 0)
    gl.glVertex3f(-d2, d2, 0)
    gl.glVertex3f(d2, d2, 0)
    gl.glVertex3f(d2, -d2, 0)
    gl.glVertex3f(-d2, -d2, 0)
    gl.glEnd()
    graphics.draw(4, gl.GL_LINE_LOOP,
                  ('v2f', (-d3, -d3, -d3, d3, d3, d3, d3, -d3)))
    gl.glDisable(gl.GL_LINE_STIPPLE)
app.run()

Линии

Рис. 4. Линии

Лицевая и нелицевая стороны полигона

Лицевая сторона будет показана, если при выводе многоугольника его вершины обходятся против часовой стрелки, в противном случае будет наблюдаться нелицевая сторона многоугольника. Каждая из сторон может быть показана либо в виде точек в вершинах многоугольника, либо в виде линий, либо залита одним цветом, либо залита с использованием интерполяции цветов, указанных в вершинах многоугольника, либо залита с использованием образца, текстуры или материала. Эти возможности, кроме трех последних, демонстрирует нижеприводимая программа, в которой первоначально левый прямоугольник выводится лицевой стороной, а правый – нелицевой, причем с интерполяцией цветов (рис. 5), поскольку вершины имеют разные цвета, а по умолчанию имеем glShadeModel(GL_SMOOTH).

GL_FRONT_AND_BACK: GL_FILL

Рис. 5. По умолчанию для GL_FRONT_AND_BACK установлено GL_FILL

Затем после нажатия на клавишу 1 лицевая сторона выводится в виде точек, а нелицевая – в виде линий (рис. 6).

После нажатия на 1

Рис. 6. После нажатия на 1

После нажатия на клавишу 2 картина меняется: лицевая сторона выводится в виде линий, а нелицевая – в виде точек (рис. 7).

После нажатия на 2

Рис. 7. После нажатия на 2

После нажатия на 3 оба прямоугольника заливаются без интерполяции цветов (рис. 8), а после нажатия на 4 получаем рис. 5.

После нажатия на 3

Рис. 8. После нажатия на 3

import pyglet
from pyglet import app, gl, graphics
from pyglet.window import Window, key
d = 12
d2 = 6
wx, wy = 1.5 * d, 1.2 * d2 # Параметры области визуализации
width, height = int(20 * wx), int(20 * wy) # Размеры окна вывода
window = Window(visible = True, width = width, height = height,
                resizable = True, caption = 'Способы вывода многоугольника')
gl.glClearColor(0.1, 0.1, 0.1, 1.0)
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
gl.glEnable(gl.GL_POINT_SMOOTH)
gl.glPointSize(16)
gl.glLineWidth(4)
@window.event
def on_draw():
    window.clear()
    gl.glMatrixMode(gl.GL_PROJECTION)
    gl.glLoadIdentity()
    gl.glOrtho(-wx, wx, -wy, wy, -1, 1)
    gl.glBegin(gl.GL_QUADS)
    gl.glColor3f(0, 1, 0)
    gl.glVertex3f(-d, -d2, 0)
    gl.glVertex3f(-1, -d2, 0)
    gl.glVertex3f(-1, d2, 0)
    gl.glVertex3f(-d, d2, 0)
    gl.glEnd()
    graphics.draw(4, gl.GL_QUADS,
                  ('v2f', (1, -d2, 1, d2, d, d2, d, -d2)),
                  ('c3f', (1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1)))
@window.event
def on_key_press(symbol, modifiers):
    mode_f = mode_b = None
    c = chr(symbol)
    if c == '1': # c == key._1
        mode_f = gl.GL_POINT
        mode_b = gl.GL_LINE
        shade_model = gl.GL_SMOOTH
    elif symbol == key._2:
        mode_f = gl.GL_LINE
        mode_b = gl.GL_POINT
        shade_model = gl.GL_SMOOTH
    elif symbol == key._3:
        mode_f = mode_b = gl.GL_FILL
        shade_model = gl.GL_FLAT
    elif symbol == key._4:
        mode_f = mode_b = gl.GL_FILL
        shade_model = gl.GL_SMOOTH
    if mode_f is not None:
        gl.glPolygonMode(gl.GL_FRONT, mode_f)
        gl.glPolygonMode(gl.GL_BACK, mode_b)
        gl.glShadeModel(shade_model)
app.run()

Заливка полигона с использованием образца

Образец размера 32*32 бита приведен на рис. 9.

Образец

Рис. 9. Образец

Он задается массивом mask из 128 элементов. Тип массива __main__.c_ubyte_Array_128. Каждые 4 элемента массива содержат 32 бита и определяют одну строку образца. Образец используется после задания

gl.glEnable(gl.GL_POLYGON_STIPPLE)
gl.glPolygonStipple(mask)

Результат показан на рис. 10.

Заливка по образцу

Рис. 10. Заливка по образцу

import pyglet
from pyglet import app, gl, graphics
from pyglet.window import Window, key
d = 12
d2 = 6
wx, wy = 1.5 * d, 1.2 * d2 # Параметры области визуализации
width, height = int(20 * wx), int(20 * wy) # Размеры окна вывода
# Муха
mask0 = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x80, 0x01, 0xc0, 0x06, 0xc0, 0x03, 0x60,
        0x04, 0x60, 0x06, 0x20, 0x04, 0x30, 0x0c, 0x20, 0x04, 0x18, 0x18, 0x20, 0x04, 0x0c, 0x30, 0x20,
        0x04, 0x06, 0x60, 0x20, 0x44, 0x03, 0xc0, 0x22, 0x44, 0x01, 0x80, 0x22, 0x44, 0x01, 0x80, 0x22,
        0x44, 0x01, 0x80, 0x22, 0x44, 0x01, 0x80, 0x22, 0x44, 0x01, 0x80, 0x22, 0x44, 0x01, 0x80, 0x22,
        0x66, 0x01, 0x80, 0x66, 0x33, 0x01, 0x80, 0xcc, 0x19, 0x81, 0x81, 0x98, 0x0c, 0xc1, 0x83, 0x30,
        0x07, 0xe1, 0x87, 0xe0, 0x03, 0x3f, 0xfc, 0xc0, 0x03, 0x31, 0x8c, 0xc0, 0x03, 0x33, 0xcc, 0xc0,
        0x06, 0x64, 0x26, 0x60, 0x0c, 0xcc, 0x33, 0x30, 0x18, 0xcc, 0x33, 0x18, 0x10, 0xc4, 0x23, 0x08,
        0x10, 0x63, 0xc6, 0x08, 0x10, 0x30, 0x0c, 0x08, 0x10, 0x18, 0x18, 0x08, 0x10, 0x00, 0x00, 0x08]
dim = 128
mask = (gl.GLubyte * dim)()
for k in range(dim):
    mask[k] = mask0[k]
window = Window(visible = True, width = width, height = height,
                resizable = True, caption = 'Заливка по образцу')
gl.glClearColor(0.1, 0.1, 0.1, 1.0)
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
@window.event
def on_draw():
    window.clear()
    gl.glMatrixMode(gl.GL_PROJECTION)
    gl.glLoadIdentity()
    gl.glOrtho(-wx, wx, -wy, wy, -1, 1)
    gl.glEnable(gl.GL_POLYGON_STIPPLE)
    gl.glPolygonStipple(mask)
    gl.glBegin(gl.GL_QUADS)
    gl.glColor3f(0, 1, 0)
    gl.glVertex3f(-d, -d2, 0)
    gl.glVertex3f(-1, -d2, 0)
    gl.glVertex3f(-1, d2, 0)
    gl.glVertex3f(-d, d2, 0)
    gl.glEnd()
    graphics.draw(4, gl.GL_QUADS,
                  ('v2f', (1, -d2, 1, d2, d, d2, d, -d2)),
                  ('c3f', (1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1)))
app.run()

Использование текстуры

Вывод текстуры регулируется ее координатами (glTexCoord2f) и параметрами (glTexParameterf). Цвет полигона определяется либо чисто текстурой, либо в результате смешения цвета полигона и текстуры (glTexEnvf).
Текстура обычно создается на основе растрового образа, загружаемого из файла (рис. 11), но может быть создана и программно (рис. 12).

Текстура

Рис. 11. Текстура на основе файла; tile_x = 2, tile_y = 1

tile_x, tile_y – соответственно число повторов текстуры по X и Y.

Текстура

Рис. 12. Программно сгенерированная текстура; tile_x = tile_y = 1

Оба результата получены следующим кодом:

from pyglet.gl import *
from pyglet import app
from pyglet.window import Window, key
d = 12
wx, wy = 1.5 * d, 1.1 * d # Параметры области визуализации
width, height = int(20 * wx), int(20 * wy) # Размеры окна вывода
texFromFile = True # True False
if texFromFile:
    tile_x = 2 # Число повторов текстуры по X
    tile_y = 1 # Число повторов текстуры по Y
else:
    tile_x = tile_y = 1
def to_c_float_Array(data): # Преобразование в си-массив
    return (GLfloat * len(data))(*data)
vld = to_c_float_Array([-d, -d, 0]) # Левая нижняя вершина
vrd = to_c_float_Array([d, -d, 0]) # Правая нижняя вершина
vru = to_c_float_Array([d, d, 0]) # Правая верхняя вершина
vlu = to_c_float_Array([-d, d, 0]) # Левая верхняя вершина
#
def texInit():
    if texFromFile:
        fn = 'G:\\python\\openGL\\кот.jpg'
        img = pyglet.image.load(fn)
        iWidth = img.width
        iHeight = img.height
        img = img.get_data('RGB', iWidth * 3)
    else:
        iWidth = iHeight = 64 # Размер текстуры равен iWidth * iHeight
        n = 3 * iWidth * iHeight
        # Каждый элемент текстуры содержит три компонента (формат текстуры GL_RGB)
        # GL_UNSIGNED_BYTE - это диапазон 0-255, поэтому для img задается GLuint
        img = np.zeros((3, iWidth, iHeight), dtype = 'uint8')
        for i in range(iHeight): # Генерация черно-белого образа, на основе которого создается текстура
         for j in range(iWidth):
            img[:, i, j] = ((i - 1) & 16 ^ (j - 1) & 16) * 255
        img = img.reshape(n)
        img = (GLubyte * n)(*img)
    p = GL_TEXTURE_2D
    r = GL_RGB
    # Задаем параметры текстуры
    glTexParameterf(p, GL_TEXTURE_WRAP_S, GL_REPEAT) # GL_CLAMP
    glTexParameterf(p, GL_TEXTURE_WRAP_T, GL_REPEAT)
    glTexParameterf(p, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
    glTexParameterf(p, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
    # Способ взаимодействия с текущим фрагментом изображения
    glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL)
    # Создаем 2d-текстуру на основе образа img
    glTexImage2D(p, 0, r, iWidth, iHeight, 0, r, GL_UNSIGNED_BYTE, img)
    glEnable(p)
window = Window(visible = True, width = width, height = height,
                resizable = True, caption = 'Текстура')
glClearColor(0.1, 0.1, 0.1, 1.0)
glClear(GL_COLOR_BUFFER_BIT)
texInit()
@window.event
def on_draw():
    window.clear()
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    glOrtho(-wx, wx, -wy, wy, -1, 1)
    glBegin(GL_QUADS)
    glTexCoord2f(0, 0);
    glVertex3fv(vld)
    glTexCoord2f(tile_x, 0)
    glVertex3fv(vrd)
    glTexCoord2f(tile_x, tile_y)
    glVertex3fv(vru)
    glTexCoord2f(0, tile_y)
    glVertex3fv(vlu)
    glEnd()
app.run()

Использование материала

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

gl.glEnable(gl.GL_LIGHTING)

Помимо этого задаются цвет материала, цвет источника света, его позиция и нормали к вершинам полигонов. На рис. 13 показан прямоугольник, в трех вершинах которого нормали обращены к источнику света, в четвертой вершине угол между нормалью и направлением света близок к 180°. По этой причине цвет этой вершины почти черный.

Материал

Рис. 13. Выполнен расчет освещенности

import pyglet
from pyglet import app, gl, graphics
from pyglet.window import Window, key
import numpy as np
from sys import exit
d = 12
wx, wy = 1.5 * d, 1.1 * d # Параметры области визуализации
width, height = int(20 * wx), int(20 * wy) # Размеры окна вывода
#
mtClr0 = [1, 1, 0, 0] # Цвет материала
light_position0 = [0, 40, 40, 0] # Позиция источника света
lghtClr0 = [0.75, 0, 0, 0] # Цвет источника света
mtClr = (gl.GLfloat * 4)()
light_position = (gl.GLfloat * 4)()
lghtClr = (gl.GLfloat * 4)()
for k in range(4): mtClr[k] = mtClr0[k]
for k in range(4): light_position[k] = light_position0[k]
for k in range(4): lghtClr[k] = lghtClr0[k]
#
window = Window(visible = True, width = width, height = height,
                resizable = True, caption = 'Материал')
gl.glClearColor(0.1, 0.1, 0.1, 1.0)
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
gl.glEnable(gl.GL_LIGHTING) # Активизируем использование материалов
@window.event
def on_draw():
    window.clear()
    gl.glMatrixMode(gl.GL_PROJECTION)
    gl.glLoadIdentity()
    gl.glOrtho(-wx, wx, -wy, wy, -1, 1)
    gl.glShadeModel(gl.GL_SMOOTH) # GL_FLAT - без интерполяции цветов
    gl.glMaterialfv(gl.GL_FRONT, gl.GL_SPECULAR, mtClr)
    gl.glLightfv(gl.GL_LIGHT0, gl.GL_POSITION, light_position)
    gl.glLightfv(gl.GL_LIGHT0, gl.GL_SPECULAR, lghtClr)
    gl.glEnable(gl.GL_LIGHT0) # Включаем в уравнение освещенности источник GL_LIGHT0
    gl.glColor3f(1, 0, 0)
    gl.glBegin(gl.GL_QUADS)
    gl.glNormal3f(0, 0, 1)
    gl.glVertex3f(-d, -d, 0)
    gl.glNormal3f(0, 0, 1)
    gl.glVertex3f(d, -d, 0)
    gl.glNormal3f(0, 0, 1)
    gl.glVertex3f(d, d, 0)
    gl.glNormal3f(0, 0, -1)
    gl.glVertex3f(-d, d, 0)
    gl.glEnd()
@window.event
def on_key_press(symbol, modifiers):
    if symbol == key._1:
        gl.glDisable(gl.GL_LIGHTING)
    elif symbol == key._2:
        gl.glEnable(gl.GL_LIGHTING)
app.run()

Цвет полигона будет определяться glColor3f, если отказаться от использования материала:

gl.glDisable(gl.GL_LIGHTING)

Это произойдет при нажатии на 1. После нажатия на 2 материал будет применен вновь.

Вывод параболоида

Выводится параболоид (рис. 14)

y = a*x2 + a*z2

Параболоид

Рис. 14. Параболоид

Нормали рассчитываются к граням в каждой вершине параболоида (гранью является трапеция).
Разницу между нормалью к грани и к вершине иллюстрирует рис. 14.

Нормаль

Рис. 15. Порядок вывода вершин, нормаль к грани и нормаль к вершине

Вычисление нормали:

import numpy as np
n = np.cross(a, b) # Векторное произведение
n = n / np.linalg.norm(n) # Нормализация
или
n = n / np.sqrt(np.sum(n**2)) # Нормализация

Замечание.OpenGL выполнит нормализацию самостоятельно, если задать:

glEnable(GL_NORMALIZE)

Нормали к грани задаются во всех вершинах (нормали к вершине не вычисляются, поэтому видны слои фигуры даже при использовании GL_SMOOTH).
Параболоид с нормалями показан на рис. 16.

С нормалями

Рис. 16. Параболоид с нормалями

После отказа от сглаживания видны отдельные грани:

GL_FLAT

Рис. 17. Параболоид: gl.glShadeModel(gl.GL_FLAT)

При выводе текстуры (сгенерированной или из файла) имеем рис. 18.

Текстура

Рис. 18. Параболоид с текстурой

# Вывод параболоида
import pyglet
from pyglet import app, gl, graphics
from pyglet.window import Window, key
import numpy as np, math
from sys import exit
# z = ac*x**2 + ac*y**2 (реально имеем: y = ac*x**2 + ac*z**2)
save_to_file = True # Флаг записи модели в файлы
read_from_file = True # Флаг чтения модели из файлов
if read_from_file: save_to_file = False
plot_surf = False # Флаг вывода поверхности z = ac*x**2 + ac*y**2
show_top = True # Флаг вывода крышки
show_normals = True
kn = 2 # Коэффициент увеличения длины отображаемой нормали
ac = 2.5 # Коэффициент в уравнении параболоида
n = 24 # Число вершин в сечении параболоида
ch = 7 # Число сечений параболоида плоскостями y = const
h_cover = 20 # Высота параболоида
vrts = [] # Координаты вершин параболоида
nrms = [] # Координаты нормалей к граням (рассчитываются для каждой вершины)
textr = [] # Координаты текстуры (задаются для каждой вершины)
vrts_top = [] # Координаты вершин крышки
textr_top = [] # Координаты тектстуры крышки
dh = h_cover / ch # Расстояние между сечениями
dal = 2 * math.pi / n # dal - угол между вершинами сечения
hp0 = 0.1 # Низ параболоида
# Текстура
use_txtr = True
texFromFile = True # True False
if use_txtr: show_normals = False
# Координаты текстуры меняются в диапазоне 0-1
dnx = 1 / (n - 1) # Шаг изменения координат текстуры по x
dny = 1 / ch # Шаг изменения координат текстуры по y
if read_from_file:
    print('Загрузка данных из двоичных файлов')
    def load_data(file, shape = None):
        with open(file, 'rb') as r_b:
            data = np.fromfile(r_b)
        if shape is not None:
            size = len(data)
            if len(shape) == 2:
                size = int(size / (shape[0] * shape[1]))
                data = data.reshape(size, shape[0], shape[1])
            else:
                size = int(size / shape[0])
                data = data.reshape(size, shape[0])
        return data
    vrts = load_data('vrts.bin', [4, 3])
    vrts_top = load_data('vrts_top.bin', [3])
    nrms = load_data('nrms.bin', [3])
    nrm_top = load_data('nrm_top.bin', None)
    textr = load_data('textr.bin', [2])
    textr_top = load_data('textr_top.bin', [2])
else:
    hp = hp0
    for i in range(ch): # Вершины
        al = 0 # Первая вершина лежит на оси Х
        s = ac * math.sqrt(hp)
        h0 = hp
        hp += dh
        s2 = ac * math.sqrt(hp)
        for j in range(n):
            co = math.cos(al)
            si = math.sin(al)
            al += dal
            co2 = math.cos(al)
            si2 = math.sin(al)
            # Координаты вершин очередной трапеции
            v0 = np.array([s * co, h0, -s * si])
            v1 = np.array([s * co2, h0, -s * si2])
            v2 = np.array([s2 * co2, hp, -s2 * si2])
            v3 = np.array([s2 * co, hp, -s2 * si])
            vrts.append([v0, v1, v2, v3])
            a = v1 - v0
            b = v3 - v0
            sab = np.cross(a, b) # Векторное произведение
            sab = sab / np.linalg.norm(sab) # Нормализация
            #sab = sab / np.sqrt(np.sum(sab**2))
            nrms.append(sab) # Координаты нормали
            textr.append([j * dnx, (h0 - hp0) / h_cover]) # Координаты текстуры
    n_vrts = len(vrts)
    # Крышка
    al = 0
    kt = 0.25
    for j in range(n):
        co = math.cos(al); si = math.sin(al)
        al += dal
        vrts_top.append([s2 * co, hp, -s2 * si])
        nrms.append(nrms[n_vrts - n + j])
        textr.append([j * dnx, 1])
        if texFromFile:
            j2 = 2 * j * dnx
            if j <= n / 4:
                textr_top.append([j2, 0.5 - j2])
            elif j <= n / 2:
                textr_top.append([j2, j2 - 0.5])
            elif j <= 3 * n / 4:
                textr_top.append([2 - j2, j2 - 0.5])
            else:
                textr_top.append([2 - j2, 2.5 - j2])
        else:
            textr_top.append([kt * co, kt * si])
    v0 = np.array(vrts_top[0])
    v1 = np.array(vrts_top[1])
    vn = np.array(vrts_top[n - 1])
    nrm_top = np.cross(v1 - v0, vn - v0) # Нормаль к крышке
    nrm_top = nrm_top / np.linalg.norm(sab)
if save_to_file:
    print('Запись данных в двоичные файлы')
    def write_to_bin(file, data):
        fn = open(file, 'wb')
        fn.write(np.array(data))
        fn.close()
    write_to_bin('vrts.bin', vrts)
    write_to_bin('vrts_top.bin', vrts_top)
    write_to_bin('nrms.bin', nrms)
    write_to_bin('nrm_top.bin', nrm_top)
    write_to_bin('textr.bin', textr)
    write_to_bin('textr_top.bin', textr_top)
if plot_surf:
    from mpl_toolkits.mplot3d import Axes3D # Для projection = '3d'
    from matplotlib import cm
    import matplotlib.pyplot as plt
    xy, z = [], []
    x_min = x_max = vrts[0][0][0]
    y_min = y_max = vrts[0][0][2]
    for quad in vrts:
        for v in quad:
            p = [v[0], v[2]]
            if not p in xy:
                xy.append(p)
                z.append(v[1])
                if p[0] < x_min: x_min = p[0]
                if p[0] > x_max: x_max = p[0]
                if p[1] < y_min: y_min = p[1]
                if p[1] > y_max: y_max = p[1]
    step = 0.5
    X = np.arange(x_min, x_max, step) # Формирование сетки
    Y = np.arange(y_min, y_max, step)
    X, Y = np.meshgrid(X, Y)
    Z = ac*X**2 + ac*Y**2 # Формируем массив Z формы (len(X), len(Y))
    fig = plt.figure()
    ax = fig.gca(projection = '3d')
    surf = ax.plot_surface(X, Y, Z, cmap = cm.plasma) # plasma Spectral
    ax.set_xlabel('X') # Метки осей координат
    ax.set_ylabel('Y')
    ax.set_zlabel('Z')
    for label in ax.xaxis.get_ticklabels(): # Настройка оси X
        label.set_color('black')
        label.set_rotation(-45)
        label.set_fontsize(9)
    for label in ax.yaxis.get_ticklabels(): # Настройка оси Y
        label.set_fontsize(9)
    for label in ax.zaxis.get_ticklabels(): # Настройка оси Z
        label.set_fontsize(9)
    ax.view_init(elev = 30, azim = 45) # Проецирование
    fig.colorbar(surf, shrink = 0.5, aspect = 5) # Шкала цветов
    plt.show() # Отображение результата
    exit()
#
def c_float_Array(data): # Преобразование в си-массив
    return (gl.GLfloat * len(data))(*data)
lghtClr0 = [0.75, 0, 0, 0]
mtClr = c_float_Array([1, 1, 0, 0])
light_position = c_float_Array([-80, 20, 90, 0])
lghtClr = c_float_Array([0.75, 0, 0, 0])
#
def texInit():
    if texFromFile:
        fn = 'G:\\python\\openGL\\кот.jpg'
        img = pyglet.image.load(fn)
        iWidth = img.width
        iHeight = img.height
        img = img.get_data('RGB', iWidth * 3)
    else:
        iWidth = iHeight = 64
        n = 3 * iWidth * iHeight
        img = np.zeros((3, iWidth, iHeight), dtype = 'uint8')
        for i in range(iHeight): # Генерация черно-белого образа, на основе которого создается текстура
         for j in range(iWidth):
            img[:, i, j] = ((i - 1) & 16 ^ (j - 1) & 16) * 255
        img = img.reshape(n)
        img = (gl.GLubyte * n)(*img)
    p = gl.GL_TEXTURE_2D
    r = gl.GL_RGB
    gl.glTexParameterf(p, gl.GL_TEXTURE_WRAP_S, gl.GL_REPEAT)
    gl.glTexParameterf(p, gl.GL_TEXTURE_WRAP_T, gl.GL_REPEAT)
    gl.glTexParameterf(p, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
    gl.glTexParameterf(p, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
    gl.glTexEnvf(gl.GL_TEXTURE_ENV, gl.GL_TEXTURE_ENV_MODE, gl.GL_DECAL)
    gl.glTexImage2D(p, 0, r, iWidth, iHeight, 0, r, gl.GL_UNSIGNED_BYTE, img)
    if use_txtr:
        gl.glEnable(p)
w = h = h_cover
width, height = 20 * w, 10 * w
window = Window(visible = True, width = width, height = height,
                resizable = True, caption = 'Параболоид')
gl.glClearColor(1, 1, 1, 1) # Белый цвет фона
gl.glClear(gl.GL_COLOR_BUFFER_BIT|gl.GL_DEPTH_BUFFER_BIT)
gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_FILL)
gl.glLineWidth(2)
gl.glPointSize(4)
gl.glEnable(gl.GL_POINT_SMOOTH)
gl.glShadeModel(gl.GL_SMOOTH) # GL_SMOOTH, GL_FLAT - без интерполяции цветов
gl.glCullFace(gl.GL_BACK) # Запрещен вывод граней, показанных нелицевой стороной
gl.glEnable(gl.GL_CULL_FACE) # Активизируем режим GL_CULL_FACE
##gl.glEnable(gl.GL_NORMALIZE)
if show_normals:
    gl.glEnable(gl.GL_DEPTH_TEST) # Активизируем тест глубины
texInit()
@window.event
def on_draw():
    window.clear()
    gl.glMatrixMode(gl.GL_PROJECTION)
    gl.glLoadIdentity()
    gl.glOrtho(-w, w, -0.5 * h, h, -w, w) # Ортографическое проецирование
    gl.glRotatef(30, 1, 0, 0) # Поворот относительно оси X
    if use_txtr and texFromFile:
        gl.glRotatef(90, 0, 1, 0) # Поворот относительно оси Y
    gl.glTranslatef(0, -7, 0) # Перенос объекта вниз (вдоль оси Y)
    gl.glMaterialfv(gl.GL_FRONT, gl.GL_SPECULAR, mtClr)
    gl.glLightfv(gl.GL_LIGHT0, gl.GL_POSITION, light_position)
    gl.glLightfv(gl.GL_LIGHT0, gl.GL_SPECULAR, lghtClr)
    gl.glEnable(gl.GL_LIGHTING) # Активизируем заданные параметры освещенности
    gl.glEnable(gl.GL_LIGHT0)
    gl.glBegin(gl.GL_QUADS)
    k = -1
    for i in range(ch):
        for j in range(n):
            k += 1
            v = vrts[k]
            v0 = v[0]; v1 = v[1]; v2 = v[2]; v3 = v[3]
            sn = nrms[k] # Нормаль к вершине (она же нормаль к грани)
            gl.glNormal3f(sn[0], sn[1], sn[2])
            tc = textr[k] # Координаты текстуры
            gl.glTexCoord2f(tc[0], tc[1])
            gl.glVertex3f(v0[0], v0[1], v0[2])
            sn = nrms[k + 1]
            gl.glNormal3f(sn[0], sn[1], sn[2])
            gl.glTexCoord2f(tc[0] + dnx, tc[1])
            gl.glVertex3f(v1[0], v1[1], v1[2])
            sn = nrms[k + n]
            gl.glNormal3f(sn[0], sn[1], sn[2])
            gl.glTexCoord2f(tc[0] + dnx, tc[1] + dny)
            gl.glVertex3f(v2[0], v2[1], v2[2])
            sn = nrms[k + n - 1]
            gl.glNormal3f(sn[0], sn[1], sn[2])
            gl.glTexCoord2f(tc[0], tc[1] + dny)
            gl.glVertex3f(v3[0], v3[1], v3[2])
    gl.glEnd() # Заканчиваем вывод боковых граней
    if show_top:
        gl.glBegin(gl.GL_POLYGON) # Вывод крышки
        gl.glNormal3f(nrm_top[0], nrm_top[1], nrm_top[2]) # Нормаль к крышке
        for j in range(n):
            v = vrts_top[j]
            tc = textr_top[j]
            gl.glTexCoord2f(tc[0], tc[1])
            gl.glVertex3f(v[0], v[1], v[2])
        gl.glEnd() # Заканчиваем вывод крышки
    if show_normals:
        # Вывод нормалей
        gl.glDisable(gl.GL_LIGHTING) # Отключаем расчет освещенности
        gl.glLineWidth(2) # Задание толщины линии
        gl.glColor3f(0, 0, 0) # Задание текущего цвета
        gl.glBegin(gl.GL_LINES)
        k = -1
        for i in range(ch):
            for j in range(n):
                k += 1
                v = vrts[k][0]
                gl.glVertex3f(v[0], v[1], v[2])
                sn = nrms[k]
                sx = kn * sn[0]
                sy = kn * sn[1]
                sz = kn * sn[2]
                gl.glVertex3f(v[0] + sx, v[1] + sy, v[2] + sz)
                # Нормали в верхнем сечении совпадают с нормалями в предыдущем сечении
                if i == ch - 1:
                    v = vrts[k][3]
                    gl.glVertex3f(v[0], v[1], v[2])
                    gl.glVertex3f(v[0] + sx, v[1] + sy, v[2] + sz)
        gl.glEnd()
        if show_top:
            gl.glColor3f(1, 1, 1) # Текущий цвет
            gl.glBegin(gl.GL_LINES) # Нормаль к крышке
            gl.glVertex3f(0, h_cover, 0)
            gl.glVertex3f(kn * nrm_top[0], h_cover + kn * nrm_top[1], kn * nrm_top[2])
            gl.glEnd()
@window.event
def on_key_press(symbol, modifiers):
    if symbol == key._1:
        gl.glDisable(gl.GL_TEXTURE_2D)
        gl.glPolygonMode(gl.GL_FRONT, gl.GL_FILL)
        gl.glShadeModel(gl.GL_SMOOTH)
        gl.glEnable(gl.GL_CULL_FACE)
    elif symbol == key._2:
        gl.glPolygonMode(gl.GL_FRONT, gl.GL_FILL)
        gl.glShadeModel(gl.GL_FLAT)
        gl.glEnable(gl.GL_CULL_FACE)
    elif symbol == key._3:
        gl.glPolygonMode(gl.GL_FRONT, gl.GL_LINE) # Вывод ребер
    elif symbol == key._4:
        gl.glPolygonMode(gl.GL_FRONT, gl.GL_POINT)
    elif symbol == key._5:
        gl.glDisable(gl.GL_CULL_FACE)
app.run()

Сведения о созданной модели будут записаны в бинарные файлы, если

save_to_file = True

и

read_from_file = False

Если read_from_file = True, то модель будет прочитана из ранее созданных бинарных файлов.
Если plot_surf = True, то по уравнению параболоида будет выведена его поверхность (рис. 19).

Параболоид

Рис. 19. Параболоид

Использование нескольких текстур

Рассматривается на примере вывода игральной кости (рис. 20).

Кость

Рис. 20. Используется 6 текстур

Текстуры создаются на основе 6 файлов, содержащих приведенные на рис. 21 изображения.

Стороны кости

Рис. 21. Стороны игральной кости

Помимо куба с текстурами, приводимая ниже программа показывает после нажатия на клавиши 2-6 разные способы вывода куба (рис. 22).

Куб

Рис. 22. После нажатия на 2, 3, ..., 6

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

elif symbol == key._4:
    ps = (gl.GLfloat * 1)()
    ps_range = (gl.GLfloat * 2)()
    gl.glGetFloatv(gl.GL_POINT_SIZE, ps) # Получаем текущий размер точки
    # Допустимый диапазон изменения размеров точки
    gl.glGetFloatv(gl.GL_POINT_SIZE_RANGE, ps_range)
    ps = ps[0]
    ps = (ps + 2) if ps < ps_range[1] - 2 else 4
    gl.glPointSize(ps)
    gl.glPolygonMode(gl.GL_FRONT, gl.GL_POINT)

Программа вывода куба с текстурой и без нее:

# Игральная кость
import pyglet
from pyglet import app, gl, graphics
from pyglet.window import Window, key
h = 20 # Половины длины ребра куба
w = 2 * h # Для задания области вывода
width = height = 300 # Размер окна вывода
textureIDs = (gl.GLuint * 6)() # Массив идентификаторов (номеров) текстур
p = gl.GL_TEXTURE_2D
tc = 1 # Число повторов текстуры
rot_x = 15 # Углы поворота вокруг осей X, Y и Z
rot_y = 25 # (15, 25, 15) или (-25, 215, -15)
rot_z = 15
verts = ((h, -h, -h), # <class 'tuple'> Координаты вершин куба
         (h, h, -h),
         (-h, h, -h),
         (-h, -h, -h),
         (h, -h, h),
         (h, h, h),
         (-h, -h, h),
         (-h, h, h))
faces = ((0, 1, 2, 3), # Индексы вершин граней куба
         (3, 2, 7, 6),
         (6, 7, 5, 4),
         (4, 5, 1, 0),
         (1, 5, 7, 2),
         (4, 0, 3, 6))
clrs = ((1, 0, 0), (0, 1, 0), (0, 0, 1), (1, 1, 0),
        (0, 1, 1), (1, 1, 1), (1, 0, 0), (0, 1, 0),
        (0, 0, 1), (1, 1, 0), (0, 1, 1), (1, 1, 1))
t_coords = ((0, 0), (0, tc), (tc, tc), (tc, 0)) # Координаты текстуры
# Индексы ребер куба (используется при выводе линий вдоль ребер куба)
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 texInit(): # Формирование текстур
    gl.glGenTextures(6, textureIDs)
    r = gl.GL_RGB
    p3 = gl.GL_REPEAT # GL_REPEAT GL_CLAMP_TO_EDGE
    p4 = gl.GL_LINEAR
    for k in range(6):
        fn = 'dice' + str(k) + '.jpg'
        img = pyglet.image.load(fn)
        iWidth = img.width
        iHeight = img.height
        img = img.get_data('RGB', iWidth * 3)
        gl.glBindTexture(p, textureIDs[k])
        gl.glTexParameterf(p, gl.GL_TEXTURE_WRAP_S, p3)
        gl.glTexParameterf(p, gl.GL_TEXTURE_WRAP_T, p3)
        gl.glTexParameterf(p, gl.GL_TEXTURE_MAG_FILTER, p4)
        gl.glTexParameterf(p, gl.GL_TEXTURE_MIN_FILTER, p4)
        gl.glTexEnvf(gl.GL_TEXTURE_ENV, gl.GL_TEXTURE_ENV_MODE, gl.GL_DECAL)
        gl.glTexImage2D(p, 0, r, iWidth, iHeight, 0, r, gl.GL_UNSIGNED_BYTE, img)
    gl.glEnable(p)
def cube_draw():
    k = -1
    for face in faces:
        k += 1
        m = -1
        v4, c4, t4 = (), (), ()
        gl.glBindTexture(p, textureIDs[k])
        for v in face:
            m += 1
            c4 += clrs[k + m]
            t4 += t_coords[m]
            v4 += verts[v]
        graphics.draw(4, gl.GL_QUADS, ('v3f', v4), ('c3f', c4), ('t2f', t4))
##    gl.glColor3f(1, 0, 0)
##    for edge in edges:
##        v2 = ()
##        for v in edge:
##            v2 += verts[v]
##        graphics.draw(2, gl.GL_LINES, ('v3f', v2))
#
window = Window(visible = True, width = width, height = height,
                resizable = True, caption = 'Куб')
gl.glClearColor(0, 0, 0, 1) # Черный цвет фона
gl.glClear(gl.GL_COLOR_BUFFER_BIT|gl.GL_DEPTH_BUFFER_BIT)
gl.glPolygonMode(gl.GL_FRONT, gl.GL_FILL)
gl.glPolygonMode(gl.GL_BACK, gl.GL_LINE)
gl.glLineWidth(3)
gl.glPointSize(4)
gl.glEnable(gl.GL_POINT_SMOOTH)
gl.glShadeModel(gl.GL_SMOOTH) # GL_SMOOTH, GL_FLAT
gl.glCullFace(gl.GL_BACK) # GL_FRONT GL_BACK
gl.glEnable(gl.GL_CULL_FACE)
gl.glEnable(gl.GL_DEPTH_TEST)
gl.glDepthFunc(gl.GL_LESS) # GL_LESS GL_GREATER
#
texInit() # Создаем текстуры
#
@window.event
def on_draw():
    window.clear()
    gl.glMatrixMode(gl.GL_PROJECTION)
    gl.glLoadIdentity()
    gl.glOrtho(-w, w, -w, w, -w, w)
    gl.glRotatef(rot_x, 1, 0, 0) # Поворот относительно оси X
    gl.glRotatef(rot_y, 0, 1, 0) # Поворот относительно оси Y
    gl.glRotatef(rot_z, 0, 0, 1) # Поворот относительно оси Z
    cube_draw()
@window.event
def on_key_press(symbol, modifiers):
    if symbol == key._1:
        gl.glPolygonMode(gl.GL_FRONT, gl.GL_FILL)
        gl.glShadeModel(gl.GL_SMOOTH)
        gl.glEnable(gl.GL_CULL_FACE)
        gl.glEnable(p)
    elif symbol == key._2:
        gl.glPolygonMode(gl.GL_FRONT, gl.GL_FILL)
        gl.glShadeModel(gl.GL_FLAT)
        gl.glEnable(gl.GL_CULL_FACE)
        gl.glDisable(p)
    elif symbol == key._3:
        gl.glPolygonMode(gl.GL_FRONT, gl.GL_LINE) # Вывод ребер
    elif symbol == key._4:
        ps = (gl.GLfloat * 1)()
        ps_range = (gl.GLfloat * 2)()
        gl.glGetFloatv(gl.GL_POINT_SIZE, ps) # Получаем текущий размер точки
        # Допустимый диапазон изменения размеров точки
        gl.glGetFloatv(gl.GL_POINT_SIZE_RANGE, ps_range)
        ps = ps[0]
        ps = (ps + 2) if ps < ps_range[1] - 2 else 4
        gl.glPointSize(ps)
        gl.glPolygonMode(gl.GL_FRONT, gl.GL_POINT)
    elif symbol == key._5:
        gl.glDisable(gl.GL_CULL_FACE)
    elif symbol == key._6:
        gl.glPolygonMode(gl.GL_FRONT, gl.GL_FILL)
        gl.glShadeModel(gl.GL_SMOOTH)
        gl.glEnable(gl.GL_CULL_FACE)
        gl.glDisable(p)
app.run()

Вращающийся куб

Выводится вращающаяся игральная кость.

Шаг угла поворота 1°

Шаг угла поворота вокруг каждой оси равен либо da, либо -da.
Смена знака шага da выполняется после заданного числа поворотов (в программе – это 180).
Процедура cube_draw, выводящая куб, вызывается после каждого срабатывания таймера:

pyglet.clock.tick()

Вызов обеспечивает метод schedule:

pyglet.clock.schedule(cube_draw)

# Вращающаяся кость
import pyglet
from pyglet import app, gl, graphics
from pyglet.window import Window
n_rot, da = 0, 1
h = 20 # Половины длины ребра куба
w = 2 * h # Для задания области вывода
width = height = 300 # Размер окна вывода
textureIDs = (gl.GLuint * 6)() # Массив идентификаторов (номеров) текстур
p = gl.GL_TEXTURE_2D
tc = 1 # Число повторов текстуры
verts = ((h, -h, -h), # Координаты вершин куба
         (h, h, -h),
         (-h, h, -h),
         (-h, -h, -h),
         (h, -h, h),
         (h, h, h),
         (-h, -h, h),
         (-h, h, h))
faces = ((0, 1, 2, 3), # Индексы вершин граней куба
         (3, 2, 7, 6),
         (6, 7, 5, 4),
         (4, 5, 1, 0),
         (1, 5, 7, 2),
         (4, 0, 3, 6))
t_coords = ((0, 0), (0, tc), (tc, tc), (tc, 0)) # Координаты текстуры
def texInit(): # Формирование текстур
    gl.glGenTextures(6, textureIDs)
    r = gl.GL_RGB
    p3 = gl.GL_REPEAT
    p4 = gl.GL_LINEAR
    for k in range(6):
        fn = 'dice' + str(k) + '.jpg'
        img = pyglet.image.load(fn)
        iWidth = img.width
        iHeight = img.height
        img = img.get_data('RGB', iWidth * 3)
        gl.glBindTexture(p, textureIDs[k])
        gl.glTexParameterf(p, gl.GL_TEXTURE_WRAP_S, p3)
        gl.glTexParameterf(p, gl.GL_TEXTURE_WRAP_T, p3)
        gl.glTexParameterf(p, gl.GL_TEXTURE_MAG_FILTER, p4)
        gl.glTexParameterf(p, gl.GL_TEXTURE_MIN_FILTER, p4)
        gl.glTexEnvf(gl.GL_TEXTURE_ENV, gl.GL_TEXTURE_ENV_MODE, gl.GL_DECAL)
        gl.glTexImage2D(p, 0, r, iWidth, iHeight, 0, r, gl.GL_UNSIGNED_BYTE, img)
    gl.glEnable(p)
def cube_draw(dt):
    k = -1
    for face in faces:
        k += 1
        m = -1
        v4, t4 = (), ()
        gl.glBindTexture(p, textureIDs[k])
        for v in face:
            m += 1
            t4 += t_coords[m]
            v4 += verts[v]
        graphics.draw(4, gl.GL_QUADS, ('v3f', v4), ('t2f', t4))
window = Window(visible = True, width = width, height = height,
                resizable = True, caption = 'Куб')
gl.glClearColor(0, 0, 0, 1) # Черный цвет фона
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
gl.glCullFace(gl.GL_BACK) # GL_FRONT GL_BACK
gl.glEnable(gl.GL_CULL_FACE)
#
texInit() # Создаем текстуры
#
@window.event
def on_draw():
    global n_rot, da
    window.clear()
    gl.glMatrixMode(gl.GL_PROJECTION)
    gl.glLoadIdentity()
    gl.glOrtho(-w, w, -w, w, -w, w)
    gl.glMatrixMode(gl.GL_MODELVIEW)
    if n_rot > 180:
        n_rot = 0
        da = -da
    n_rot += 1    
    gl.glRotatef(da, 1, 0, 0)
    gl.glRotatef(da, 0, 1, 0)
    gl.glRotatef(da, 0, 0, 1)
    pyglet.clock.tick()
#
pyglet.clock.schedule(cube_draw)
app.run()

Тест глубины

На рис. 23 показаны два прямоугольника с одинаковыми размерами, но разными z-координатами вершин: z-координата вершин красного прямоугольника (КП) равна 1, синего прямоугольника (СП) – 0.

Тест глубины

Рис. 23. Тест глубины отключен

СП перекрывает КП, хотя z-координата вершин КП больше, чем у СП (1 против 0). То есть должно быть ровно наоборот: КП перекрывает СП.
Однако этого не происходит, поскольку в приводимой ниже программе, выводящей СП и КП, первоначально отключен тест глубины:

glDisable(GL_DEPTH_TEST),

а СП выводится вслед за КП.
При нажатии на 1 включается тест глубины:

glEnable(GL_DEPTH_TEST),

и вывод осуществляется с учетом z-координат вершин СП и КП (рис. 24).

Тест глубины

Рис. 24. Выполняется тест глубины

При нажатии на 2 тест глубины отключается и получаем рис. 23.

# Тест глубины
import pyglet
from pyglet import app, gl, graphics
from pyglet.window import Window, key
d = 20 # Половина длины большой стороны прямоугольника
d2 = d / 2
z2 = 1 # z-координата вершин красного прямоугольника
wx, wy = 2 * d, 2 * d2 # Для задания области вывода
width, height = 300, 150 # Размеры окна вывода
def draw():
    gl.glMatrixMode(gl.GL_PROJECTION)
    gl.glLoadIdentity()
    gl.glOrtho(-wx, wx, -wy, wy, -2, 2)
    gl.glMatrixMode(gl.GL_MODELVIEW)
    gl.glLoadIdentity()
    gl.glTranslatef(-d/2, -d2/2, 0)
    gl.glColor3f(1, 0, 0) # Вывод красного прямоугольника (КП)
    graphics.draw(4, gl.GL_QUADS, ('v3f', (-d, -d2, z2, d, -d2, z2, d, d2, z2, -d, d2, z2)))
    gl.glTranslatef(d, d2, 0)
    gl.glColor3f(0, 0, 1) # Вывод синего прямоугольника (СП)
    graphics.draw(4, gl.GL_QUADS, ('v2f', (-d, -d2, d, -d2, d, d2, -d, d2)))
#
window = Window(width = width, height = height, caption = 'Тест глубины')
gl.glClearColor(0.75, 0.75, 0.75, 1) # Серый цвет фона
gl.glClear(gl.GL_COLOR_BUFFER_BIT|gl.GL_DEPTH_BUFFER_BIT)
gl.glDisable(gl.GL_DEPTH_TEST)
gl.glDepthFunc(gl.GL_LESS) # GL_LESS GL_GREATER
#
@window.event
def on_draw():
    window.clear()
    draw()
@window.event
def on_key_press(symbol, modifiers):
    if symbol == key._1:
        gl.glEnable(gl.GL_DEPTH_TEST)
    elif symbol == key._2:
        gl.glDisable(gl.GL_DEPTH_TEST)
app.run()

Два материала и источника света

На рис. 25 показан результат применения двух материалов и двух источников света.
Первые материал и источник света применяются к левому прямоугольнику, вторые – к правому.

Два материала

Рис. 25. Два материала и источника света. Нормали рассчитаны к граням

При левая грань выведена с применением первой пары материала и источника света:

gl.glMaterialfv(gl.GL_FRONT, gl.GL_SPECULAR, mtClr)
gl.glEnable(gl.GL_LIGHT0)
gl.glDisable(gl.GL_LIGHT1)

правая – с применением второй пары материала и источника света:

gl.glMaterialfv(gl.GL_FRONT, gl.GL_SPECULAR, mtClr2)
gl.glDisable(gl.GL_LIGHT0)
gl.glEnable(gl.GL_LIGHT1)

Цвета первых материала и света соответственно

[0.7, 0.9, 0, 0] и [1, 0.5, 0.5, 0],

а вторых

[0, 0.7, 0.9, 0] и [0.5, 0.5, 1.0, 0].

Координаты первого и второго источников света соответственно

[-100, 200, 400, 0] и [100, 200, 400, 0].

При одновременной работе обоих источников света получаем рис. 26.

Два света

Рис. 26. Два материала и источника света. Включены оба источника

Контрольные вопросы и задачи

ЛР 2. Решаются задачи 2 и 18

  1. Примитивы OpenGL.
  2. Способы вывода точки.
  3. Различие между GL_LINES, GL_LINE_LOOP и GL_LINE_STRIP.
  4. Как при выводе линии задать и использовать шаблон.
  5. Лицевая и нелицевая стороны полигона.
  6. Способы вывода полигона.
  7. Как задать вывод лицевой стороны в виде точек, нелицевой в виде линий.
  8. Как задать заливку лицевой стороны, а нелицевую вывести в виде точек.
  9. Как задать вывод с интерполяцией цветов и без нее.
  10. Параметры метода draw.
  11. Назначение glBegin / glEnd.

ЛР 3. Решается задача 19

  1. Виды проецирования.
  2. Как задать в OpenGL изометрическую проекцию 3d-объекта (разновидность аксонометрической проекции).
  3. Реализация аффинных преобразований координат в OpenGL.
  4. Расчет цвета пикселя отрезка и полигона при линейной интерполяции цветов, заданных в вершинах примитива.
  5. Как рассчитать нормали к граням и вершинам:

    Нормали

  6. Задание материалов и источников света.
  7. Компоненты света, задаваемые GL_AMBIENT, GL_DIFFUSE и GL_SPECULAR.
  8. Вывести в задаче 19 полигоны, используя draw.
  9. Задачи, решаемые с применением компьютерной графики.

ЛР 4. Решается задача 22

Наложение текстуры на грани призмы.

Текстура

  1. Преобразование координат.
  2. Схема наложения текстуры.
  3. Определение принадлежности точки многоугольнику.
  4. Алгоритм растровой развертки полигона.
  5. Использование нескольких текстур.
  6. Расчет координат текстуры при выводе n-угольника.
  7. Приводимая ниже программа выводит черно-белое изображение с искаженной текстурой (левая трапеция).
    Внести изменения в программу, чтобы получилось черно-желтое изображение с выправленной текстурой (правая трапеция).

Правка текстуры

from pyglet.gl import *
import pyglet
from pyglet import app, graphics
from pyglet.window import Window, key
import numpy as np
d, d1, d2 = 5, 10, 15
wx, wy = 1.5 * d2, 1.5 * d2
width, height = int(30 * wx), int(30 * wy)
window = Window(visible = True, width = width, height = height, resizable = True)
glClearColor(0.1, 0.1, 0.1, 1.0)
glClear(GL_COLOR_BUFFER_BIT)
def texInit():
    iWidth = iHeight = 64
    n = 3 * iWidth * iHeight
    img = np.zeros((3, iWidth, iHeight), dtype = 'uint8')
    for i in range(iHeight):
     for j in range(iWidth):
        img[:, i, j] = ((i - 1) & 16 ^ (j - 1) & 16) * 255
    img = img.reshape(n)
    img = (GLubyte * n)(*img)
    p, r = GL_TEXTURE_2D, GL_RGB
    glTexParameterf(p, GL_TEXTURE_WRAP_S, GL_REPEAT)
    glTexParameterf(p, GL_TEXTURE_WRAP_T, GL_REPEAT)
    glTexParameterf(p, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
    glTexParameterf(p, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
    glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL)
    glTexImage2D(p, 0, r, iWidth, iHeight, 0, r, GL_UNSIGNED_BYTE, img)
    glEnable(p)
texInit()
zv = -d2/2
v0, v1, v2, v3 = (-d2,d2,zv), (-d1,d1,0), (d1,d1,0), (d2,d2,zv)
@window.event
def on_draw():
    window.clear()
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    glOrtho(-wx, wx, -wy, wy, -20, 20)
    glRotatef(90, 1, 0, 0)
    graphics.draw(4, GL_QUADS, ('v3f', (v0 + v1 + v2 + v3)), ('t2f', (0,1, 0,0, 1,0, 1,1)))
app.run()

ЛР 5 и 6. Решаются задачи 200 и 203

  1. Форма массива:
  2. Отказ от вывода граней, показанных нелицевой (лицевой) стороной (GL_CULL_FACE):
  3. Тест глубины (GL_DEPTH_TEST):
  4. Задание изометрической проекции.
  5. С какой целью расчет модели выносится за пределы @window.event-процедур.
  6. Использованные в программе объекты, хранящие сведения о модели фигуры.
  7. Вывести оси координат, инвариантные к аффинным преобразованиям, употребляя glPushMatrix() и gl.glPopMatrix().
  8. Перенести расчет цвета вершин за пределы on_draw.
  9. Перейти от GL_QUADS к GL_QUAD_STRIP (GL_TRIANGLES к GL_TRIANGLE_STRIP или GL_TRIANGLE_FAN).
  10. Используя средства matplotlib, вывести полусферу (если поверхность не имеет уравнения).
  11. Отобразить нормали.

ЛР 7. Решается задача 8

Состоит из двух частей. В первой ведется работа с многослойным перцептроном, во второй – со сверточными слоями.

Требования к отчету:

  1. Решаемая задача.
  2. Описание набора данных MNIST.
  3. Примеры изображений MNIST с указанием метки над изображениями.
  4. Описание слоев исходной нейронной сети (результат model.summary()).
  5. Таблица с результатами всех использованных вариантов НС. Имеет следующие столбцы: Замер времени:
    import time
    start = time.time()
    <Вычисления>
    print('Время вычислений:', time.time() - start)
  6. Графики обучения лучшего и худшего по точности на оценочном множестве варианта нейронной сети.

Контрольные вопросы и задачи:

  1. Модель нейрона. Назначение функции активации и смещения.
  2. Модель многослойного перцептрона.
  3. Схема обучение классификатора.
  4. Данные и метки в задачи классификации рукописных цифр. Число классов.
    Обучающее и оценочное множества.
    Что содержат x_train, y_train, x_test, y_test и каков тип этих объектов.
    http://yann.lecun.com/exdb/mnist/
    http://100byte.ru/python/MNIST_NN/mnist_nn.html#p1
    http://100byte.ru/python/imgClasses/imgClasses.html#p1
  5. Формы входных данных при наличии слоя Flatten или Reshape и без них.
  6. Вывести обобщенный портрет рукописной цифры MNIST обучающего (оценочного) множеств.
    Обобщенный портрет формируется следующим образом:
    берутся и суммируются массивы с описаниями заданной цифры на заданном множестве (обучающее или оценочное).
    (Данные предварительно приводятся к диапазону 0-1, тип float32).
    Результат делится на число примеров цифры в заданном множестве.
    Полученные данные выводятся в виде изображения.
  7. Категориальное (one-hot, двоичное) представление метки. Привести пример.
  8. Назначение слоев Flatten и Reshape.
  9. Что такое функция активации и ее назначение. Какие функции активации использованы. Их графики. http://100byte.ru/python/factors/factors.html#p1_3
  10. Параметры units, activation, use_bias, kernel_initializer, bias_initializer слоя Dense (http://100byte.ru/python/factors/factors.html#p1).
  11. Какие параметры в модели нейронной сети меняются при ее обучении.
  12. Назначение функции потерь. Какие функции потерь использованы. Формулы вычисления потерь. К чему может привести замена одной функции потерь на другую (http://100byte.ru/python/loss/loss.html).
  13. Параметры x, y, batch_size, epochs, verbose, shuffle метода fit. Что возвращает метод fit (http://100byte.ru/python/factors/factors.html#p3).
  14. Критерий качества обучения нейронной сети. Различие между acc и val_acc.
  15. Как проявляется переобучение и способы его преодоления (http://100byte.ru/python/MNIST_NN/mnist_nn.html#p10).
  16. Вывести 50 примеров заданной цифры из заданного множества (обучающего или оценочного) MNIST (5 строк по 10 примеров в каждой и заголовок над каждым изображением).
  17. Почему кроме оценочного применяется и тестовое множество.
  18. Как формируется выход сверточных слоев и слоев подвыборки.
  19. Как работает слой прореживания.
  20. Для чего может быть полезна генерация данных, выполняемая ImageDataGenerator.
  21. Назначение ранней остановки и порядок ее задания.
  22. Профессии, в которых ИИ может вытеснить человека.
  23. Сферы деятельности человека, в которых ИИ его превосходит.
  24. Виды деятельности, в которых человек не может быть в ближайшей перспективе заменен ИИ.

ЛР 8. Решаются задачи 224 и 225

Состоит из двух частей. В первой создается и анализируется набор данных с зашумленных изображениями кривых и геометрических фигур, во второй – обучается НС, классифицирующая изображения созданного в первой части набора данных.

Отчет по задаче 224 включает:

  1. Номер варианта и задание.
  2. Диапазоны изменения варьируемых параметров.
  3. Описание приемов, использованных при внесении шума в генерируемые изображения.
  4. Примеры сгенерированных изображений (по 10 в каждом классе).
  5. Алгоритм проверки класса на неповторяемость.
  6. Результаты проверки классов на неповторяемость (в виде таблицы).
  7. Обоснование значения ИСР_мин (ИСР – индекс структурного различия), применяемого при поиске похожих изображений.
  8. Программа генерации, визуализации и проверки классов на неповторяемость.

Отчет по задаче 225 включает:

  1. Условие решаемой задачи.
  2. Примеры загруженных изображений (по одному из каждого класса).
  3. Описание (символьное) модели НС, обеспечивающей точность классификации 100 % на оценочном множестве, графики обучения этой НС.
  4. Описание (символьное) модели НС с минимальной архитектурой, обеспечивающей ту же точность классификации, что и прежняя НС (см. п. выше), и графики обучения второй сети.
  5. Программы загрузки набора данных, визуализации изображений, создания и обучения НС.

Контрольные вопросы и задачи:

  1. Приемы, использованные при генерации зашумленных изображений.
  2. Требования к набору данных.
  3. Как обосновать значение ИСР_мин (ИСР – индекс структурного различия), применяемого при поиске похожих изображений.
  4. Как выявить похожие изображения.
  5. Вычислить среднее значение ИСР изображений заданного класса в заданном множестве MNIST (обучающем или проверочном).
  6. В заданном множестве (обучающее или проверочное) вывести по 10 случайных изображений каждого класса.
  7. Обучить НС, забирая из обучающего множества 10% данных в оценочное (параметр validation_split):
    model.fit(x_train, y_train, batch_size = batch_size, epochs = epochs, validation_split = 0.1)
    Точность модели оценить затем на проверочном множестве (применить evaluate).
  8. Вывести рисунки неверно классифицированных изображений (использовать predict).
  9. Объединить наборы данных двух полностью отличающихся вариантов и обучить НС для классификации изображений полученного набора данных.
  10. Алгоритм отсечения отрезков Коэна-Сазерленда.
  11. Алгоритм разбиения средней точкой в задаче отсечения отрезков.
  12. Алгоритм отсечения отрезков Кируса – Бека.
  13. Алгоритм центрирования изображения.
  14. Вычислить число ошибок классификации в каждом классе и вывести список классов, упорядоченный по числу ошибок классификации.
    Элемент списка: (класс / число ошибок).

Литература

1. OpenGL: преобразование координат. [Электронный ресурс] URL: http://100byte.ru/100btwrks/prjctn/prjctn.html. (Дата обращения: 01.09.2020).
2. Вывод граней OpenGL. [Электронный ресурс] URL: http://100byte.ru/stdntswrks/gl/gl.html. (Дата обращения: 01.09.2020).
3. C#, OpenGL: вывод стандартных примитивов. [Электронный ресурс] URL: http://100byte.ru/stdntswrks/cshrp/stdprmtvs/stdprmtvs.html. (Дата обращения: 01.09.2020).
4. Построение, текстурирование и тонирование сферы. [Электронный ресурс] URL: http://100byte.ru/stdntswrks/cshrp/sphTK/sphTK.html. (Дата обращения: 01.09.2020).
5. Шейдер с иллюзией выступов и впадин. [Электронный ресурс] URL: http://100byte.ru/stdntswrks/cshrp/mapTK/mapTK.html. (Дата обращения: 01.09.2020).
6. Шейдер с картой-кубом в задаче воспроизведения глобуса.Шейдер с картой-кубом в задаче воспроизведения глобуса. [Электронный ресурс] URL: http://100byte.ru/stdntswrks/cshrp/earthCubeMapTK/earthCubeMapTK.html. (Дата обращения: 01.09.2020).
7. Шейдерная реализация освещения по Блинну и Фонгу. [Электронный ресурс] URL: http://100byte.ru/stdntswrks/cshrp/blinnPhongPart/blinnPhong.html. (Дата обращения: 01.09.2020).
8. Вывод текста.. [Электронный ресурс] URL: http://100byte.ru/stdntswrks/cshrp/txt.html. (Дата обращения: 01.09.2020).
9. Вывод частиц. [Электронный ресурс] URL: http://100byte.ru/stdntswrks/cshrp/pf.html. (Дата обращения: 01.09.2020).
10. Руководство по программированию. [Электронный ресурс] URL: https://pyglet.readthedocs.io/en/latest/programming_guide/graphics.html. (Дата обращения: 01.09.2020).

Список работ

Рейтинг@Mail.ru