Python + GLFW + OpenGL

# スタートまで
## 底本
GLUT/freeglutによる OpenGL入門／床井 浩平／工学社
です。
## 環境
主はLinux Mint，副でWindows。
## OpenGL
あちこちで「古の」とか「今さら」とか書かれているglBegin方式です。
## ベースとなるコード
「pyglfw tutorial」で検索して出てきたページ [Python で OpenGL (2) PyOpenGLのインストール](https://maku.blog/p/665rua3/) をスタートにする。エッセンスだけを残すと，

```
#   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()
```

ところが，システムモニターを動かしながらこれを実行すると，CPUコアの1個が100%になってしまう。`poll_events()`は一瞬で終わるが，これを繰り返し呼び続けるからだ。少し休みを入れるといい。というわけで

```
import time

    time.sleep(1e-3)
```
を入れてやればよい。が，こんなことを自分でやらなければならないのか。用意されていた。

```
    glfw.wait_events_timeout(1e-3)
```

である。よって，先ほどのコードの`poll_events()`を`glfw.wait_events_timeout(1e-3)`にしたものをベースにする。
【後日追記】
と思ったら違った。`glfw.wait_events_timeout(1e-3)`ではなく，`glfw.wait_events()`である。ちゃんとリファレンスを読まないとダメですね。

> 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.

と書いてある（この文はリファレンスではなく，ガイド）。このページのコードは直した。イベント・ドリヴンのループは高速に回さなければいけないと思い込んでいた。
`glfw.wait_events_timeout()`はアニメーションで有用である。

なお，何もしないのだからloopは`pass`でもいいのかというと，プログラムを終了するために押す×のイベントさえ受付けなくなり，終了させる手段がなくなる。×を押し続けると「Programから応答がありません」になってしまう。
## まずはバージョンの取得

```Python: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()
```

自分の環境ではGLFWのバージョンはLinux Mintが3.2.1，Windowsが3.3.2だった。（2020/4/26）

が。古めのマシン2台でWindows上で
The driver does not appear to support OpenGL.
というメッセージが出てクラッシュした。いずれもグラフィックスはCPU内蔵である。
1台はi3 M380で，OpenGL Extension Viewerというソフトで見るとOpenGL Versionの項がN/Aなので，当然である。
もう1台はi5-2500で，ViewerはVersion 3.1と言っているのだが，ViewerのRendering Testをするとクラッシュする。グラフィックシステムがOpenGLをサポートしていることになっているのにちゃんとできていないようである。
# イベント
イベント・ドリヴンであるから，まずはイベントを知らなければいけない。event.pyはウィンドウ関係の重要な2つのイベントwindow_sizeとwindow_refreshを検知するプログラムである。GLFWではイベントにcallbackを結びつける関数は`glfw.set_xxx_callback`という名前である。これらの関数の第1引数はwindowであるから，windowが作られた後（`glfw.create_window`の後）にこれらを呼ぶことになる。

```Python: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()
```

さて，Windows OS上でこれを実行すると，ウィンドウは作られるのに，イベントは何も発生していなことがわかる。なお，Linux上ではrefreshが発生する。OSにより動作が異なるのは大変困ったことである。一つのコードで両方のOSで動くようにするためには，OSを検出するか，発生しないWindowsに合わせることになる。（追記： OSの違いではなく，GLFWのバージョンの違いかもしれません。）

さて，考えてみると，refreshはあくまでもre-（再）である。このような場面でよく使われるexposeではない。最初の描画はあんただよ，というのがGLFWの言い分なのかもしれない。また，sizeについてはglfw.create_windowを呼ぶときに我々が指定する。初期サイズはあんたが知っているよね，というのがGLFWの言い分なのかもしれない。

このようなことから，初期動作をcallbackに任せっぱなしにできないのが面倒なところである。

次に，ウィンドウを一旦アイコン化してからまた開くと，Linuxではイベントは何も発生しないが，Windowsでは発生する。他のウィンドウで当該ウィンドウの一部を隠してからまた前面に出したりする際には何もイベントは発生しない。

次に，マウス操作でウィンドウのサイズを静かに少しだけ変えてみると，sizeとrefreshのイベントがペアで，この順で発生することがわかる。

フル版も載せておく。

```Python: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()
```
# 静止画の基本
全く動きのない静止画の基本となるプログラムである。ファイル名中の5.1は底本によっており，気にしないでいただきたい。

```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()
```
描画は初期とwindow_refreshの際に必要なので，displayという名前でcallback関数window_refreshからは独立させてある。

GLFWでは初めからダブル・バッファリングなので，display関数の最後はglfw.swap_buffersである。

ところで，関数make_context_currentは何をしているのか。コンテクストを「現在の」にする…引数はwindowである。一つのプログラムは複数のウィンドウを持つことができる。現在の操作はどのウィンドウに対してなのか，を決めるのがmake_context_currentである。2wins.pyは2つのウィンドウを持つプログラムである。この場合は，一方のウィンドウでずっとイベントを待っているわけにはいかないので，`glfw.wait_events`ではなく，`glfw.wait_events_timeout()`を使用している。

```Python: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()
```

# 入力
## マウス
イベントは以下のようなものである。
cursor_pos　カーソルが移動したとき
cursor_enter　カーソルが当該ウィンドウに入ったとき，出たとき
mouse_button　マウスのボタンが押されたとき
scroll　マウス中央のダイヤルを回したとき（y），左右に倒したとき（x）

GLFWでは，座標x, yは得られないので，座標が必要であればglfw.get_cursor_posで取得する。

```Python: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()
```
## キーボード
keyで得られるのはkey codeという整数値である。だが，〇〇というキーのキーコードはいくらだろう，ということを知る必要はない。すべてのキーに定数が付けられている。プログラムではA，上向きカーソルキー，エンターの例を示している。

```Python: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()
```
# Referenceの見方
pyglfwの [プロジェクトページと見られるページ](https://pypi.org/project/pyglfw/) には大したことは書かれていない。このページにホームページというリンクがあるが，リンク切れである。Mirrorというページはある。このようなページにReferenceがあってほしいのだが，どうも見当たらない。

そこで，[GLFWのページ](https://www.glfw.org/) を見てみる。ここは Documentation > HTML Documentation と進んでいくとTutorialもReferenceもある。

問題は，GLFWのページはC言語で書かれているが，それがPythonではどうなるか，ということである。

今までのものを関数で見比べてみると，
glfwCreateWindow → glfw.create_window
こんな感じである。キャメルケース → スネークケースである。
CとPythonで引数の数が違うことがある。Pythonの glfw.get_cursor_pos(window) は引数が1つ，windowだけで，座標をタプルで返したが，Cでは
void glfwGetCursorPos(GLFWwindow * window, double * xpos, double * ypos);
という形になっている。

定数については，GLFW_MOUSE_BUTTON_LEFT → glfw.MOUSE_BUTTON_LEFT といった感じである。

【後日追記 2020/5/19】
関数の引数については上に「大したことは書かれていない」と書いたpyglfwのプロジェクトページの Libapi の項に書かれていた。
# 初期にウィンドウサイズに対処が必要なプログラム
このようなコードになろう。関数resizeにおいて，Windowsではアイコン化の際にwindow_sizeイベントが発生し，ウィンドウサイズが0になることに対する処置が必要である。

```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()
```
# アニメーション
ループにおいて，動作中であれば次の描画をして，poll_events，そうでなければwait_events_timeout，という方法でできる。動作中のとき，poll_eventsではなく，wait_events_timeoutにすると遅くなる。
冒頭にあるDOUBLE_BUFFERINGをFalseにするとシングル・バッファリングになり，速い代わりにちらつく。

```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()
```