PyOpenGLでモダンな表示を行う

# 概要
約十年ぶりにOpenGLを触ることになり、glBegin()で三角形でも書いていけばいいかと思っていたら、そんな書き方はもう古いらしいことが分かりました（[参考](https://keiorogiken.wordpress.com/2018/02/20/modern%E3%81%AAopengl%E3%82%92%E6%9B%B8%E3%81%8D%E3%81%BE%E3%81%97%E3%82%87%E3%81%86/)）。
さらに今回対象とするターゲットがPythonだったこともあり、簡単な図形の表示を行うにも一苦労したので、一通りの手順をまとめておこうと思います。

# 進め方
モダンなOpenGLへの変更点やモダンさの詳細は様々なWebページで解説がなされているので、ここでは実際に表示するプログラムを書くことに注力していきます。
具体的には[ここ](http://marina.sys.wakayama-u.ac.jp/~tokoi/?date=20090909)で、作成されている立方体を描画するC++のプログラムをPython化することで処理の流れとPythonでの実装を見ていきます。

# 環境

- ubuntu 20 on docker
- apt

  ```bash
  apt install -y python3 python3-pip freeglut3 freeglut3-dev libglew-dev
  ```
- requirements.txt

```
pip
numpy==1.20.2
PyOpenGL==3.1.5
PyOpenGL-accelerate==3.1.5
scipy==1.6.2
```

# ソースコード
全体のソースコードは[ここ](https://github.com/Keiji2827/PyOpenGLDraw)に置いています。
## main
mainは初期化とloopを呼び出すだけ。
RenderクラスのコンストラクタでGLUTの初期化を実行しています。

```python
# メインプログラム
def main():
    render = Render(sys.argv[0])
    render.init()
    render.mainloop()
```

## class Render
実際の処理を行うクラス。
メンバ関数はでもとのソースコードにほぼ対応しています。

- init()
- display()
- mainloop()

### init()

```python
    def init(self):
        # 背景色
        glClearColor(1.0, 1.0, 1.0, 1.0)

        with open(self.vertShaderPath, "r") as f:
            vertShaderData = f.read()

        with open(self.fragShaderPath, "r") as f:
            fragShaderData = f.read()

        # シェーダオブジェクトの作成
        vertShader = glCreateShader(GL_VERTEX_SHADER)
        fragShader = glCreateShader(GL_FRAGMENT_SHADER)

        # シェーダのソースプログラムの読み込み
        glShaderSource(vertShader, vertShaderData)
        glShaderSource(fragShader, fragShaderData)
        
        # バーテックスシェーダのソースプログラムのコンパイル
        glCompileShader(vertShader)
        compiled = glGetShaderiv(vertShader, GL_COMPILE_STATUS)
        if compiled == GL_FALSE:
            print("in cimpiled error")
            strInfoLog = glGetShaderInfoLog(vertShader)
            print("Compilation failure for "  + " shader:\n" + str(strInfoLog))

        # フラグメントシェーダのソースプログラムのコンパイル
        glCompileShader(fragShader)
        compiled = glGetShaderiv(fragShader, GL_COMPILE_STATUS)
        if compiled == GL_FALSE:
            print("in cimpiled error")
            strInfoLog = glGetShaderInfoLog(fragShader)
            print("Compilation failure for "  + " shader:\n" + str(strInfoLog))

        # プログラムオブジェクトの作成
        self.gl2Program = glCreateProgram()

        # シェーダオブジェクトのシェーダプログラムへの登録
        glAttachShader(self.gl2Program, vertShader)
        glAttachShader(self.gl2Program, fragShader)

        # attribute 変数 position の index を 0 に指定する。
        glBindAttribLocation(self.gl2Program, 0, "position")

        # シェーダプログラムのリンク
        glLinkProgram(self.gl2Program)
        linked = glGetProgramiv(self.gl2Program, GL_LINK_STATUS)
        if linked == GL_FALSE:
            print("Link error.")
            strInfoLog = printProgramInfoLog(self.gl2Program)
            return

        # シェーダオブジェクトのデタッチ
        glDetachShader(self.gl2Program, vertShader)
        glDetachShader(self.gl2Program, fragShader)
        # シェーダオブジェクトの削除
        glDeleteShader(vertShader)
        glDeleteShader(fragShader)
```
今回の苦労したシェーダの読み込み。
リストなどを使用したらもう少しすっきりするとは思いますが、学習のために愚直に書いています。


```python
        # 視野変換行列を求める
        temp0 = matrix.lookAt(4.0, 5.0, 6.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0)

        # 透視投影変換行列を求める 
        temp1 = matrix.cameraMatrix(30.0, 1.0, 7.0, 11.0)

        # 視野変換行列と投影変換行列の積を projectionMatrix に入れる
        self.projectionMatrix = matrix.multiplyMatrix(temp0, temp1)

        # uniform 変数 projectionMatrix の場所を得る
        self.projectionMatrixLocation = glGetUniformLocation(self.gl2Program, "projectionMatrix")
```
ここは古いOpenGLに近いところ。
mtrixは行列計算をしている別ファイルを参照してください。


```python
        # 頂点バッファオブジェクトを２つ作る
        self.vertbuffer = glGenBuffers(1)
        self.fragbuffer = glGenBuffers(1)

        # 頂点バッファオブジェクトに８頂点分のメモリ領域を確保する 
        glBindBuffer(GL_ARRAY_BUFFER, self.vertbuffer)
        glBufferData(GL_ARRAY_BUFFER, position, GL_STATIC_DRAW)

        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, self.fragbuffer)
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, edge.astype(np.int32), GL_STATIC_DRAW)

        # 頂点バッファオブジェクトを解放する 
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)

        # 頂点バッファオブジェクトを解放する
        glBindBuffer(GL_ARRAY_BUFFER, 0)
```
モダンな書き方ですね。
`glBufferData()`などはC++のものと引数が若干異なっています。

### display()

```python
    def display(self):
        # 画面クリア
        glClear(GL_COLOR_BUFFER_BIT)

        # シェーダプログラムを適用する
        glUseProgram(self.gl2Program)

        # uniform 変数 projectionMatrix に行列を設定する
        glUniformMatrix4fv(self.projectionMatrixLocation, 1, GL_FALSE, self.projectionMatrix)

        # index が 0 の attribute 変数に頂点情報を対応付ける
        glEnableVertexAttribArray(0)

        # 頂点バッファオブジェクトとして vertbuffer を指定する
        glBindBuffer(GL_ARRAY_BUFFER, self.vertbuffer)

        # 頂点情報の格納場所と書式を指定する
        glVertexAttribPointer(0, 3, GL_DOUBLE, GL_FALSE, 0, None)

        # 頂点バッファオブジェクトの指標として fragbuffer を指定する
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, self.fragbuffer)

        # 図形を描く 
        glDrawElements(GL_LINES, 24, GL_UNSIGNED_INT, None)

        # 頂点バッファオブジェクトを解放する
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)
        glBindBuffer(GL_ARRAY_BUFFER, 0)

        # index が 0 の attribute 変数の頂点情報との対応付けを解除する
        glDisableVertexAttribArray(0)

        glutSwapBuffers()
```

# まとめ
モダンなOpenGLの実装をPythonでのPyOpenGLを使って学びました。
正直写経しただけで分かっていないところも多いのですが、全体の流れは把握できたと思います。
モダンなOpenGLでは、シェーダを使ってGPUを有効活用できるという大きな利点はありますが、その分今回のような簡単な図形を書いたりするために手間がかかるようになっている気がします。
近年では簡単な図形の描画もUnityなどのツールを使うことが多く、OpenGLを使う人向けに導入コストを下げることがコスト増になるのかもしれません。
考え方もモダンにしていかなくてはとつくづく思います。