PythonでVAOによるGLSLシェーダープログラミング!

PythonOpenGL
連載一覧 "Python3で始めるOpenGL4"

OpenGL 2.1 でも似たような記事を書いていたので、壮絶なデジャブを感じているyosiです。

今回はGLSLとPythonの感動的な出会いを演出していきたいと思います。

本当はVAOとGLSLは別々の回に紹介しようと思っていたのですが、3.x以降VAOは必須のようで確かに無いとエラーが出ます。

GLSLの説明をするためにはVAOの知識がいるし、VAOの説明をするためにはGLSLの説明がいるという、卵と鶏の関係を作り上げていて困ります。

こういう仕様があるとみなさんも一度は聞いたことがあるであろう「これのプログラムはおまじないだから気にしないでね(テヘッ」という説明をするのが手っ取り早くなってしまいますが、この教え方は嫌いなのでやる気はしません。

幸いPythonを使っているのでOpenGL以外の部分に関する説明が簡潔に済みます。

なので、今回はVAOとGLSLの両方を説明していきます。

かなり重要な機能なので、頑張っていきましょう!

GLSLとは

GLSLとはOpenGLで使うシェーダー言語です。

これを使うことによって効率的かつ柔軟に描画を高速で行うことができます。

GLSLの言語自体はC言語をベースにしています。

GLSLはバーテックスシェーダとフラグメントシェーダの2種類があります。

それぞれ頂点ごと、ピクセルごとに処理を行います。

VAO(vertex array object)

VAOはOpenGL2.xからある機能VBO(Vertex Buffer Object)を簡潔に書けるようにした機能です。

詳しく説明していきます。

画面を更新するたびに頂点座標配列や法線配列のすべてをGPU側に送っていると、CPU-GPU間の通信量をあっという間に使い切ってしまいます。

そこで2.xからは描画前にそれらの情報をあらかじめGPU側に送っておくVBOが実装されました。

これは頂点座標配列や法線配列ごとにバッファオブジェクトというものを作って、描画のたびにそれら1個づつ有効にしなければなりませんでした。

しかしVAOは、複数のバッファオブジェクトをバインドした1つのVAOオブジェクトを有効にすれば済むようになりました。

そのVAOオブジェクトを作成していきます。

送るデータは3点の座標データと色データです。

positions = np.array([
	[0.0, 0.5, 0.0, 1.0], 
	[0.5, -0.5, 0.0, 1.0], 
	[-0.5, -0.5, 0.0, 1.0]], dtype=np.float32)
colors = np.array([
	[1.0, 0.0, 0.0, 1.0], 
	[0.0, 1.0, 0.0, 1.0], 
	[0.0, 0.0, 1.0, 1.0]], dtype=np.float32)

バッファオブジェクトを作成してデータをGPU側に送ります。

# 座標バッファオブジェクトを作成してデータをGPU側に送る
position_vbo = glGenBuffers(1)
glBindBuffer(GL_ARRAY_BUFFER, position_vbo)
glBufferData(GL_ARRAY_BUFFER, positions.nbytes, positions, GL_STATIC_DRAW)

# 色バッファオブジェクトを作成してデータをGPU側に送る
color_vbo = glGenBuffers(1)
glBindBuffer(GL_ARRAY_BUFFER, color_vbo)
glBufferData(GL_ARRAY_BUFFER, colors.nbytes, colors, GL_STATIC_DRAW)

glBindBuffer(GL_ARRAY_BUFFER, 0)

VAOをバインドしておきます。

# VAOを作成してバインド
vao = glGenVertexArrays(1)
glBindVertexArray(vao)

これをアンバインドするまでにVBOを指定していくことで、複数のVBOをカプセル化することができます。

頂点インデックス配列もVAOに格納できます。
第1引数が違うことに注意してください。

# 0と1のアトリビュート変数を有効化
glEnableVertexAttribArray(0)
glEnableVertexAttribArray(1)

# 座標バッファオブジェクトの位置を指定(location = 0)
glBindBuffer(GL_ARRAY_BUFFER, position_vbo)
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, None)

# 色バッファオブジェクトの位置を指定(location = 1)
glBindBuffer(GL_ARRAY_BUFFER, color_vbo)
glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, None)

# インデックスオブジェクトを作成してデータをGPU側に送る
indices = np.array([0, 1, 2], dtype=np.uint)
index_vbo = glGenBuffers(1)
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, index_vbo)
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices, GL_STATIC_DRAW)

# バッファオブジェクトとVAOをアンバインド
glBindBuffer(GL_ARRAY_BUFFER, 0)
glBindVertexArray(0)

これでVAOオブジェクトは完成です。

今回は頂点配列データは座標と色の2つのみでしたが、法線など他の配列データを使いたいときは座標や色などと同じ要領で行います。

シンプルなGLSLプログラム

描画を行うにはバーテックスシェーダとフラグメントシェーダの2種類のシェーダーが必要です。

バーテックスシェーダ

まずはVAOによって転送されたデータを使うバーテックスシェーダから作っていきます。

File -> New -> File からテキストファイルを作成します。

僕は「shader.vert」というファイル名にしました。

コード全体は次のようになります。

#version 400 core

layout(location = 0) in vec4 position;
layout(location = 1) in vec4 color;
out vec4 outColor;

void main(void){
    outColor = color;
    gl_Position = position;
}

1行目は GLSL の400のコアプロファイルバージョン( OpenGL 4.0 のコアプロファイルに対応)を使っているということです。

次に location が0と1の座標と色情報を受け取っています。
受け取る変数には in を付けます。

逆にフラグメントシェーダに渡す変数には out を付けます。

バーテックスシェーダは頂点ごとに実行されます。

つまり頂点の数(今回は3回)だけ実行されます。

メイン関数の中では、フラグメントシェーダに渡す変数に outColor に受け取った色情報を入れてます。

gl_Position はGLSLの組み込み変数で表示する頂点座標をここに渡します。

 

フラグメントシェーダ

同様に「shader.frag」といった名前のファイルを作成します。

コード全体は次のようになります。

#version 400 core

in vec4 outColor;
out vec4 outFragmentColor;

void main(void){
    outFragmentColor = outColor;
}

フラグメントシェーダは最終的に表示されるピクセルごとに表示されます。

今回表示するウィンドウは640×480なので307200回呼び出されます。

outFragmentColorはそれぞれのピクセルで最終的に表示する色を代入します。

GLSLファイルを読み込もう!

例えばC言語を使うときソースファイルの拡張子は .c となりますしC++の時は .cpp 、Javaの時は .java になると思います。

バーテックスシェーダとフラグメントシェーダのファイルの拡張子はそれぞれ.vert と .frag になることが多いです。

多いと書いたのは、実はGLSLのソースファイルはただのテキストファイルなんです。

「いやいや、どの言語のソースファイルだってテキストファイルでしょ」と言われればまあそうなんですが、GLSLの場合は僕たち自身がC++やPythonでソースファイルから文字列として読み込んで、その上コンパイルなどをしなければなりません。なんというクソ仕様

これには訳があって、端末ごとに最適なコンパイルをするためらしいです。

理想はわかりますけれど、初心者のハードルが間違いなく上がってます(;^_^A

しかし今回使っているのはPython!

簡潔に書けますので恐れる必要はありません。

Pythonでバーテックスシェーダファイルを読み込むには

f = open(vertex_shader_file, 'r', encoding='utf-8')
vertex_shader_src = f.read()
f.close()

のようにしますがこれではcloseし忘れてしまうかもしれないので

# シェーダーファイルからソースコードを読み込む
with open(vertex_shader_file, 'r', encoding='utf-8') as f:
    vertex_shader_src = f.read()

のように書きます。

同様にフラグメントシェーダファイルも読み込みます。

with open(fragment_shader_file, 'r', encoding='utf-8') as f:
    fragment_shader_src = f.read()

作成したシェーダオブジェクトにソースコードを渡しコンパイルする作業を両方のシェーダに行います。

vertex_shader = glCreateShader(GL_VERTEX_SHADER)
glShaderSource(vertex_shader, vertex_shader_src)
glCompileShader(vertex_shader)

fragment_shader = glCreateShader(GL_FRAGMENT_SHADER)
glShaderSource(fragment_shader, fragment_shader_src)
glCompileShader(fragment_shader)

プログラムオブジェクト作成しアタッチします。
使い終わったシェーダオブジェクトは破棄します。

program = glCreateProgram()
glAttachShader(program, vertex_shader)
glDeleteShader(vertex_shader)
glAttachShader(program, fragment_shader)
glDeleteShader(fragment_shader)

作成したプログラムオブジェクトをリンクして完成です。

glLinkProgram(program)

今回はPythonで書いたので簡潔ですがC++で書くとこんな感じでかなり長ったらしくなります。

C言語で書くともっと厄介です(僕は書く気になれないので和歌山大学の床井先生のホームページなどを参考にしてください)。

シェーダとVAOを利用して描画する

先ほどコンパイルしたシェーダを有効にするには

glUseProgram(program)

の様に書きます。

VAOをバインドしてglDrawElementsで描画します。

プログラム全体

一部は関数化してます。

from OpenGL.GL import *
import glfw
import numpy as np


def create_program(vertex_shader_file, fragment_shader_file):
    # シェーダーファイルからソースコードを読み込む
    with open(vertex_shader_file, 'r', encoding='utf-8') as f:
        vertex_shader_src = f.read()

    # 作成したシェーダオブジェクトにソースコードを渡しコンパイルする
    vertex_shader = glCreateShader(GL_VERTEX_SHADER)
    glShaderSource(vertex_shader, vertex_shader_src)
    glCompileShader(vertex_shader)

    with open(fragment_shader_file, 'r', encoding='utf-8') as f:
        fragment_shader_src = f.read()

    fragment_shader = glCreateShader(GL_FRAGMENT_SHADER)
    glShaderSource(fragment_shader, fragment_shader_src)
    glCompileShader(fragment_shader)

    # プログラムオブジェクト作成しアタッチ
    program = glCreateProgram()
    glAttachShader(program, vertex_shader)
    glDeleteShader(vertex_shader)
    glAttachShader(program, fragment_shader)
    glDeleteShader(fragment_shader)

    # 作成したプログラムオブジェクトをリンク
    glLinkProgram(program)

    return program


def create_vao():
    indices = np.array([0, 1, 2], dtype=np.uint)
    positions = np.array([[0.0, 0.5, 0.0, 1.0], [0.5, -0.5, 0.0, 1.0], [-0.5, -0.5, 0.0, 1.0]], dtype=np.float32)
    colors = np.array([[1.0, 0.0, 0.0, 1.0], [0.0, 1.0, 0.0, 1.0], [0.0, 0.0, 1.0, 1.0]], dtype=np.float32)

    # 座標バッファオブジェクトを作成してデータをGPU側に送る
    position_vbo = glGenBuffers(1)
    glBindBuffer(GL_ARRAY_BUFFER, position_vbo)
    glBufferData(GL_ARRAY_BUFFER, positions.nbytes, positions, GL_STATIC_DRAW)

    # 色バッファオブジェクトを作成してデータをGPU側に送る
    color_vbo = glGenBuffers(1)
    glBindBuffer(GL_ARRAY_BUFFER, color_vbo)
    glBufferData(GL_ARRAY_BUFFER, colors.nbytes, colors, GL_STATIC_DRAW)

    glBindBuffer(GL_ARRAY_BUFFER, 0)

    # VAOを作成してバインド
    vao = glGenVertexArrays(1)
    glBindVertexArray(vao)

    # 0と1のアトリビュート変数を有効化
    glEnableVertexAttribArray(0)
    glEnableVertexAttribArray(1)

    # 座標バッファオブジェクトの位置を指定(location = 0)
    glBindBuffer(GL_ARRAY_BUFFER, position_vbo)
    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, None)

    # 色バッファオブジェクトの位置を指定(location = 1)
    glBindBuffer(GL_ARRAY_BUFFER, color_vbo)
    glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, None)

    # インデックスオブジェクトを作成してデータをGPU側に送る
    index_vbo = glGenBuffers(1)
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, index_vbo)
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices, GL_STATIC_DRAW)

    # バッファオブジェクトとVAOをアンバインド
    glBindBuffer(GL_ARRAY_BUFFER, 0)
    glBindVertexArray(0)

    return vao


def main():
    # GLFW初期化
    if not glfw.init():
        return

    # ウィンドウを作成
    window = glfw.create_window(640, 480, 'Hello World', None, None)
    if not window:
        glfw.terminate()
        print('Failed to create window')
        return

    # コンテキストを作成
    glfw.make_context_current(window)

    # バージョンを指定
    glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 4)
    glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, 0)
    glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE)

    program = create_program('shader.vert', 'shader.frag')
    vao = create_vao()

    while not glfw.window_should_close(window):
        # バッファを指定色で初期化
        glClearColor(0, 0, 0, 1)
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

        # シェーダを有効化
        glUseProgram(program)

        glBindVertexArray(vao)

        # バインドしたVAOを用いて描画
        glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, None)

        glBindVertexArray(0)

        # バッファを入れ替えて画面を更新
        glfw.swap_buffers(window)

        # イベントを受け付けます
        glfw.poll_events()

    # ウィンドウを破棄してGLFWを終了
    glfw.destroy_window(window)
    glfw.terminate()


# Pythonのメイン関数はこんな感じで書きます
if __name__ == "__main__":
    main()

結果

このような結果になると思います。

VAOなど難しかったかもしれませんが、一度作ってしまえばもう悩むことも少ないと思います。

main.zip

連載一覧 "Python3で始めるOpenGL4"

あわせて読みたい