Python + GLFW + OpenGL

Until the start

Bottom book

Introduction to OpenGL with GLUT / freeglut / Kohei Tokai / Engineering Co., Ltd. is.

environment

Mainly Linux Mint, secondary Windows. OpenGL It is a glBegin method that is written here and there as "old" or "now".

Base code

Start the page OpenGL (2) Install PyOpenGL with Python that appears after searching for "pyglfw tutorial". If you leave only the essence,

#   based on https://maku.blog/p/665rua3/

import glfw
from OpenGL.GL import *

if not glfw.init():
    raise RuntimeError('Could not initialize GLFW3')

window = glfw.create_window(300, 300, 'Program', None, None)
if not window:
    glfw.terminate()
    raise RuntimeError('Could not create an window')

glfw.make_context_current(window)

while not glfw.window_should_close(window):
    glfw.poll_events()

glfw.terminate()

However, if you do this while running System Monitor, one of the CPU cores will be 100%. poll_events () ends in an instant, but it keeps calling this repeatedly. It's a good idea to take a break. so

import time

    time.sleep(1e-3)

Just put in. But do I have to do this myself? It was prepared.

    glfw.wait_events_timeout(1e-3)

Is. Therefore, it is based on the previous code poll_events () changed to glfw.wait_events_timeout (1e-3). [Postscript] I thought it was different. Not glfw.wait_events_timeout (1e-3), but glfw.wait_events (). You have to read the reference properly.

It puts the thread to sleep until at least one event has been received and then processes all received events. This saves a great deal of CPU cycles and is useful for, for example, editing tools.

(This sentence is a guide, not a reference). The code on this page has been fixed. I was convinced that the Event Driven loop had to run fast. glfw.wait_events_timeout () is useful in animations.

The loop can be pass because it does nothing. It doesn't even accept the x event that you press to end the program, and there is no way to end it. If you keep pressing x, you will get "No response from Program".

First, get the version

version.py


import glfw
from OpenGL.GL import *

print('GLFW version:', glfw.get_version())

if not glfw.init():
    raise RuntimeError('Could not initialize GLFW3')

window = glfw.create_window(300, 300, 'prog1 5.1', None, None)
if not window:
    glfw.terminate()
    raise RuntimeError('Could not create an window')

glfw.make_context_current(window)

print('Vendor :', glGetString(GL_VENDOR))
print('GPU :', glGetString(GL_RENDERER))
print('OpenGL version :', glGetString(GL_VERSION))

glfw.terminate()

In my environment, the GLFW version was 3.2.1 for Linux Mint and 3.3.2 for Windows. (2020/4/26)

But. On Windows with two old machines The driver does not appear to support OpenGL. I got the message and crashed. In both cases, the graphics are built into the CPU. One is i3 M380, which is natural because the OpenGL Version section is N / A when viewed with software called OpenGL Extension Viewer. The other is i5-2500, and Viewer says Version 3.1, but when I do Rendering Test of Viewer, it crashes. The graphics system is supposed to support OpenGL, but it doesn't seem to work.

Event

Since it is an event driven, you must first know the event. event.py is a program that detects two important window-related events, window_size and window_refresh. In GLFW, the function that associates a callback with an event is named glfw.set_xxx_callback. Since the first argument of these functions is window, we will call them after the window is created (after glfw.create_window).

win_event.py


#    mini version

#    size is NOT invoked at start.
#    refresh is NOT invoked at start on Windows.

import glfw
from OpenGL.GL import *

def init():
    glClearColor(0.0, 0.0, 1.0, 1.0)

def window_size(window, w, h):
    global n_size
    n_size += 1
    print('Size', n_size, w, h)

def window_refresh(window):
    global n_refresh
    n_refresh += 1
    print('Refresh', n_refresh)

n_size = 0
n_refresh = 0

if not glfw.init():
    raise RuntimeError('Could not initialize GLFW3')

window = glfw.create_window(300, 300, 'Window events', None, None)
if not window:
    glfw.terminate()
    raise RuntimeError('Could not create an window')

glfw.set_window_size_callback(window, window_size)
glfw.set_window_refresh_callback(window, window_refresh)
glfw.make_context_current(window)

init()

while not glfw.window_should_close(window):
    glfw.wait_events()

glfw.terminate()

Now, when you run this on a Windows OS, you can see that the window is created, but no event has occurred. Note that refresh occurs on Linux. It is very troublesome that the operation differs depending on the OS. In order to make it work on both OSs with one code, it is necessary to detect the OS or match it with Windows that does not occur. (Addition: It may be a difference in GLFW version, not a difference in OS.)

Now, when you think about it, refresh is just re-. It is not an expose that is often used in such situations. It may be GLFW's argument that the first drawing is just that. We also specify size when calling glfw.create_window. It may be GLFW's argument that you know the initial size.

For this reason, it is troublesome that the initial operation cannot be left to callback.

Next, if you iconify the window and then open it again, no event will occur on Linux, but it will occur on Windows. No event occurs when a part of the window is hidden by another window and then brought to the front again.

Next, if you gently change the window size slightly by operating the mouse, you can see that the size and refresh events occur in pairs in this order.

I will also post the full version.

win_event_full.py


#    full version

#    size is NOT invoked at start.
#    refresh is NOT invoked at start on Windows.
#    size is invoked at iconify on Windows.

import glfw
from OpenGL.GL import *

def init():
    glClearColor(0.0, 0.0, 1.0, 1.0)

def window_size(window, w, h):
    global n_size
    n_size += 1
    print('Size', n_size, w, h)

def framebuffer_size(window, w, h):
    global n_FB_size
    n_FB_size += 1
    print('Framebuffer Size', n_FB_size, w, h)

def window_pos(window, x, y):
    global n_pos
    n_pos += 1
    print('Position', n_pos, x, y)

def window_iconify(window, iconified):
    global n_icon
    n_icon += 1
    print('Iconify', n_size, iconified)

def window_refresh(window):
    global n_refresh
    n_refresh += 1
    print('Refresh', n_refresh)

n_size = 0
n_FB_size = 0
n_pos = 0
n_icon = 0
n_refresh = 0

if not glfw.init():
    raise RuntimeError('Could not initialize GLFW3')

window = glfw.create_window(300, 300, 'Window events', None, None)
if not window:
    glfw.terminate()
    raise RuntimeError('Could not create an window')

glfw.set_window_size_callback(window, window_size)
glfw.set_framebuffer_size_callback(window, framebuffer_size)
glfw.set_window_pos_callback(window, window_pos)
glfw.set_window_iconify_callback(window, window_iconify)
glfw.set_window_refresh_callback(window, window_refresh)
glfw.make_context_current(window)

init()

while not glfw.window_should_close(window):
    glfw.wait_events()

glfw.terminate()

Still image basics

This is the basic program for still images that do not move at all. 5.1 in the file name depends on the base, so don't worry.

Python:prog1_5.1_GLFW.py


import glfw
from OpenGL.GL import *

def display():
    glClear(GL_COLOR_BUFFER_BIT)

    glBegin(GL_LINE_LOOP)
    glVertex2d(-0.9, -0.9)
    glVertex2d( 0.9, -0.9)
    glVertex2d( 0.9,  0.9)
    glVertex2d(-0.9,  0.9)
    glEnd()

    glfw.swap_buffers(window)

def init():
    glClearColor(0.0, 0.0, 1.0, 1.0)
    display()   # necessary only on Windows

def window_refresh(window):
    display()

if not glfw.init():
    raise RuntimeError('Could not initialize GLFW3')

window = glfw.create_window(300, 300, 'prog1 5.1', None, None)
if not window:
    glfw.terminate()
    raise RuntimeError('Could not create an window')

glfw.set_window_refresh_callback(window, window_refresh)
glfw.make_context_current(window)

init()

while not glfw.window_should_close(window):
    glfw.wait_events()

glfw.terminate()

Since drawing is necessary for the initial stage and window_refresh, it is named display and is independent of the callback function window_refresh.

Since GLFW is double buffering from the beginning, the end of the display function is glfw.swap_buffers.

By the way, what is the function make_context_current doing? Make the context "current" ... The argument is window. A program can have multiple windows. It is make_context_current that determines which window the current operation is for. 2wins.py is a program with two windows. In this case, we can't wait for an event all the time in one window, so we're using glfw.wait_events_timeout () instead of glfw.wait_events.

2wins.py


import glfw
from OpenGL.GL import *


class GLwin:
    def __init__(self, title, xpos, ypos, window_refresh_fun, init_fun):
        self.window = glfw.create_window(300, 300, title, None, None)
        if not self.window:
            glfw.terminate()
            raise RuntimeError('Could not create an window')
        glfw.set_window_pos(self.window, xpos, ypos)
        glfw.set_window_refresh_callback(self.window, window_refresh_fun)
        glfw.make_context_current(self.window)
        init_fun(self.window)

    def window_should_close(self):
        return glfw.window_should_close(self.window)

    def wait_events_timeout(self):
        glfw.make_context_current(self.window)
        glfw.wait_events_timeout(1e-3)

def display1(window):
    glClear(GL_COLOR_BUFFER_BIT)

    glLineWidth(15)
    glColor3d(1.0, 1.0, 0.0)
    glBegin(GL_LINES)
    glVertex2d(-0.8, 0.0)
    glVertex2d( 0.8, 0.0)
    glEnd()

    glfw.swap_buffers(window)

def display2(window):
    glClear(GL_COLOR_BUFFER_BIT)

    glLineWidth(15)
    glColor3d(1.0, 0.0, 1.0)
    glBegin(GL_LINES)
    glVertex2d(0.0, -0.8)
    glVertex2d(0.0,  0.8)
    glEnd()

    glfw.swap_buffers(window)

def init1(window):
    glClearColor(1.0, 0.0, 0.0, 1.0)
    display1(window)   # necessary only on Windows

def init2(window):
    glClearColor(0.0, 1.0, 0.0, 1.0)
    display2(window)   # necessary only on Windows

def window_refresh1(window):
    display1(window)

def window_refresh2(window):
    display2(window)


if not glfw.init():
    raise RuntimeError('Could not initialize GLFW3')

glwin1 = GLwin('Window 1', 100, 200, window_refresh1, init1)
glwin2 = GLwin('Window 2', 450, 200, window_refresh2, init2)

while not ( glwin1.window_should_close() or glwin2.window_should_close() ):
    glwin1.wait_events_timeout()
    glwin2.wait_events_timeout()

glfw.terminate()

input

mouse

The event is as follows. cursor_pos When the cursor moves cursor_enter When the cursor enters or exits the window mouse_button When the mouse button is pressed scroll When the dial in the center of the mouse is turned (y), when it is tilted left or right (x)

In GLFW, the coordinates x and y cannot be obtained, so if the coordinates are needed, obtain them with glfw.get_cursor_pos.

mouse_event.py


import glfw
from OpenGL.GL import *

def init():
    glClearColor(0.0, 0.0, 1.0, 1.0)
    display()   # necessary only on Windows

def display():
    glClear(GL_COLOR_BUFFER_BIT)
    glfw.swap_buffers(window)

def cursor_pos(window, xpos, ypos):
    print('cursor_pos:', xpos, ypos)

def cursor_enter(window, entered):
    print('cursor_enter:', entered)

def mouse_button(window, button, action, mods):
    pos = glfw.get_cursor_pos(window)
    print('mouse:', button, end='')

    if button == glfw.MOUSE_BUTTON_LEFT:
        print('(Left)', end='')
    if button == glfw.MOUSE_BUTTON_RIGHT:
        print('(Right)', end='')
    if button == glfw.MOUSE_BUTTON_MIDDLE:
        print('(Middle)', end='')

    if action == glfw.PRESS:
        print(' press')
    elif action == glfw.RELEASE:
        print(' release')
    else:
        print(' hogehoge')

    x, y = pos
    print(pos, x, y)

def scroll(window, xoffset, yoffset):
    print('scroll:', xoffset, yoffset)

def window_refresh(window):
    display()


if not glfw.init():
    raise RuntimeError('Could not initialize GLFW3')

window = glfw.create_window(300, 300, 'mouse on GLFW', None, None)
if not window:
    glfw.terminate()
    raise RuntimeError('Could not create an window')

glfw.set_cursor_pos_callback(window, cursor_pos)
glfw.set_cursor_enter_callback(window, cursor_enter)
glfw.set_mouse_button_callback(window, mouse_button)
glfw.set_scroll_callback(window, scroll)
glfw.set_window_refresh_callback(window, window_refresh)
glfw.make_context_current(window)

init()

while not glfw.window_should_close(window):
    glfw.wait_events()

glfw.terminate()

keyboard

What you get with key is an integer value called key code. But you don't need to know how much the key code for the key XX is. All keys have constants. The program shows examples of A, the up cursor key, and enter.

keyboard.py


import glfw
from OpenGL.GL import *

def display():
    glClear(GL_COLOR_BUFFER_BIT)
    glfw.swap_buffers(window)

def init():
    glClearColor(0.0, 0.0, 1.0, 1.0)
    display()   # necessary only on Windows

def keyboard(window, key, scancode, action, mods):
    print('KB:', key, chr(key), end=' ')
    if action == glfw.PRESS:
        print('press')
    elif action == glfw.REPEAT:
        print('repeat')
    elif action == glfw.RELEASE:
        print('release')
    if key == glfw.KEY_A:
        print('This is A.')
    elif key == glfw.KEY_UP:
        print('This is Up.')
    elif key == glfw.KEY_ENTER:
        print('This is Enter.')

def window_refresh(window):
    display()


if not glfw.init():
    raise RuntimeError('Could not initialize GLFW3')

window = glfw.create_window(300, 300, 'KB on GLFW', None, None)
if not window:
    glfw.terminate()
    raise RuntimeError('Could not create an window')

glfw.set_key_callback(window, keyboard)
glfw.set_window_refresh_callback(window, window_refresh)
glfw.make_context_current(window)

init()

while not glfw.window_should_close(window):
    glfw.wait_events()

glfw.terminate()

How to read Reference

The pyglfw pages that appear to be project pages doesn't say much. There is a link called homepage on this page, but the link is broken. There is a page called Mirror. I'd like to see a Reference on a page like this, but I can't find it.

So, take a look at the GLFW page. If you go to Documentation> HTML Documentation, you will find Tutorial and Reference.

The question is, the GLFW page is written in C, but what happens in Python?

If you compare the previous ones with functions, glfwCreateWindow → glfw.create_window It looks like this. Camel case → Snake case. The number of arguments may differ between C and Python. Python's glfw.get_cursor_pos (window) has one argument, only window, and returns the coordinates as a tuple, but in C void glfwGetCursorPos(GLFWwindow * window, double * xpos, double * ypos); It is in the form of.

For constants, it looks like GLFW_MOUSE_BUTTON_LEFT → glfw.MOUSE_BUTTON_LEFT.

Programs that need to deal with window size initially

Let's have a code like this. In the function resize, in Windows, it is necessary to take measures against the window_size event occurring at the time of iconification and the window size becoming 0.

[Postscript 2020/5/19] The function arguments were described in the Libapi section of the pyglfw project page, which said "No big deal" above.

Python:8.4_GLFW.py


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

W_INIT = 360
H_INIT = 300

VERTEX = [
    [0.0, 0.0, 0.0],   # A
    [1.0, 0.0, 0.0],   # B
    [1.0, 1.0, 0.0],   # C
    [0.0, 1.0, 0.0],   # D
    [0.0, 0.0, 1.0],   # E
    [1.0, 0.0, 1.0],   # F
    [1.0, 1.0, 1.0],   # G
    [0.0, 1.0, 1.0]    # H
]

EDGE = [
    [0, 1], # a (A-B)
    [1, 2], # i (B-C)
    [2, 3], # u (C-D)
    [3, 0], # e (D-A)
    [4, 5], # o (E-F)
    [5, 6], # ka (F-G)
    [6, 7], # ki (G-H)
    [7, 4], # ku (H-E)
    [0, 4], # ke (A-E)
    [1, 5], # ko (B-F)
    [2, 6], # sa (C-G)
    [3, 7]  # shi (D-H)
]

def display():
    glClear(GL_COLOR_BUFFER_BIT)

    glBegin(GL_LINES)
    for edge1 in EDGE:
        for i in edge1:
            glVertex3dv(VERTEX[i])
    glEnd()

    glfw.swap_buffers(window)

def set_view(w, h):
    glLoadIdentity()
    gluPerspective(35.0, w/h, 1.0, 100.0)
    gluLookAt(3.0, 4.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0)

def resize(window, w, h):
    # for iconify on Windows
    if h==0:
        return
    glViewport(0, 0, w, h)
    set_view(w, h)

def init():
    gray = 0.6
    glClearColor(gray, gray, gray, 1.0)
    set_view(W_INIT, H_INIT)
    display()   # necessary only on Windows

def window_refresh(window):
    display()

if not glfw.init():
    raise RuntimeError('Could not initialize GLFW3')

window = glfw.create_window(W_INIT, H_INIT, 'Wireframe', None, None)
if not window:
    glfw.terminate()
    raise RuntimeError('Could not create an window')

glfw.set_window_size_callback(window, resize)
glfw.set_window_refresh_callback(window, window_refresh)
glfw.make_context_current(window)

init()

while not glfw.window_should_close(window):
    glfw.wait_events()

glfw.terminate()

animation

In the loop, if it is running, the following drawing can be done, poll_events, otherwise wait_events_timeout. When it is running, it will be slower if you set wait_events_timeout instead of poll_events. Setting DOUBLE_BUFFERING at the beginning to False results in single buffering, which flickers instead of being fast.

Python:9.1-2_GLFW.py


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

DOUBLE_BUFFERING = True

W_INIT = 320
H_INIT = 240

VERTEX = [
    [0.0, 0.0, 0.0],   # A
    [1.0, 0.0, 0.0],   # B
    [1.0, 1.0, 0.0],   # C
    [0.0, 1.0, 0.0],   # D
    [0.0, 0.0, 1.0],   # E
    [1.0, 0.0, 1.0],   # F
    [1.0, 1.0, 1.0],   # G
    [0.0, 1.0, 1.0]    # H
]

EDGE = [
    [0, 1], # a (A-B)
    [1, 2], # i (B-C)
    [2, 3], # u (C-D)
    [3, 0], # e (D-A)
    [4, 5], # o (E-F)
    [5, 6], # ka (F-G)
    [6, 7], # ki (G-H)
    [7, 4], # ku (H-E)
    [0, 4], # ke (A-E)
    [1, 5], # ko (B-F)
    [2, 6], # sa (C-G)
    [3, 7]  # shi (D-H)
]

def display():
    global r

    glClear(GL_COLOR_BUFFER_BIT)

    glLoadIdentity()
    gluLookAt(3.0, 4.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0)
    glRotated(r, 0.0, 1.0, 0.0)
    
    glColor3d(0.0, 0.0, 0.0)
    glBegin(GL_LINES)
    for edge1 in EDGE:
        for i in edge1:
            glVertex3dv(VERTEX[i])
    glEnd()

    if DOUBLE_BUFFERING:
        glfw.swap_buffers(window)
    else:
        glFlush()
    
    r += 1
    if r==360:
        r = 0

def set_view(w, h):
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    gluPerspective(35.0, w/h, 1.0, 100.0)
    glMatrixMode(GL_MODELVIEW)

def resize(window, w, h):
    # for iconify on Windows
    if h==0:
        return
    glViewport(0, 0, w, h)
    set_view(w, h)

def mouse_button(window, button, action, mods):
    global rotation
    if action == glfw.PRESS:
        rotation = ( button == glfw.MOUSE_BUTTON_LEFT )

def init():
    gray = 0.8
    glClearColor(gray, gray, gray, 1.0)
    set_view(W_INIT, H_INIT)
    display()   # necessary only on Windows

def window_refresh(window): # for resize
    display()


r = 0
rotation = False

if not glfw.init():
    raise RuntimeError('Could not initialize GLFW3')

if not DOUBLE_BUFFERING:
    glfw.window_hint(glfw.DOUBLEBUFFER, glfw.FALSE)

window = glfw.create_window(W_INIT, H_INIT, 'Animation on GLFW', None, None)
if not window:
    glfw.terminate()
    raise RuntimeError('Could not create an window')

glfw.set_mouse_button_callback(window, mouse_button)
glfw.set_window_size_callback(window, resize)
glfw.set_window_refresh_callback(window, window_refresh)
glfw.make_context_current(window)

init()

while not glfw.window_should_close(window):
    if rotation:
        display()
        glfw.poll_events()
#        glfw.wait_events_timeout(1e-2)
    else:
        glfw.wait_events_timeout(1e-3)

glfw.terminate()

Recommended Posts

Python + GLFW + OpenGL
Python
Raspberry Pi + Python + OpenGL memo
kafka python
Python basics ⑤
python + lottery 6
Python Summary
Built-in python
Python comprehension
Python technique
Python 2.7 Countdown
Python memorandum
Python FlowFishMaster
Python service
python tips
python function ①
Python basics
Python memo
ufo-> python (3)
Python comprehension
install python
Python Singleton
Python basics ④
Python Memorandum 2
python memo
Python Jinja2
Python increment
atCoder 173 Python
[Python] function
Python installation
Installing Python 3.4.3.
Try python
Python memo
Python iterative
Python algorithm
Python2 + word2vec
[Python] Variables
Python functions
Python sys.intern ()
Python tutorial
Python decimals
python underscore
Python summary
Start python
[Python] Sort
Note: Python
Python basics ③
python log
Python basics
[Scraping] Python scraping
Python update (2.6-> 2.7)
python memo
Python memorandum
Python # sort
ufo-> python
Python nslookup
python learning
Hannari Python 2020
[Rpmbuild] Python 3.7.3.
Prorate Python (1)
python memorandum