単純なシェーダープログラムの作成¶
今までのサンプルは古い OpenGL の仕様に基づいたコードなので、いずれ無用の存在になる(私のノートはこういうパターンがけっこう多い)。全てを忘れて最新バージョンの OpenGL が推奨する様式でコードを書くことにしたい。残念ながら私の環境は OpenGL 3.1 までの機能しかサポートしていないので、今後ここに記すコードはせいぜい 3.1 程度の新しさとなる。
私の OpenGL の知識はバージョン 1.4 程度で止まっており、ましてやシェーダーなどは触ったこともない。ゆえに、ここでは初歩的な事項の確認にとどまる。
Warning
私の環境でプログラムが適切なグラフィックドライバーを取得できない不具合が発生しており、本稿のスクリプトを実行すると、次のエラーメッセージが生じて異常終了する。ちなみに description のテキストは「無効な列挙」だ。
bash$ ./shaderdemo.py
freeglut (./shaderdemo.py): OpenGL >2.1 context requested but wglCreateContextAttribsARB is not available! Falling back to legacycontext creation
freeglut (./shaderdemo.py): fgInitGL2: fghGenBuffers is NULL
Vendor: Microsoft Corporation
Renderer: GDI Generic
Version: 1.1.0
Traceback (most recent call last):
File "./shaderdemo.py", line 214, in <module>
sys.exit(main(sys.argv))
File "./shaderdemo.py", line 211, in main
app.run(sys.argv)
File "D:\home\yojyo\devel\all-note\notebook\source\_sample\pyopengl\appbase.py", line 69, in run
self.init_glut(args)
File "D:\home\yojyo\devel\all-note\notebook\source\_sample\pyopengl\appbase.py", line 120, in init_glut
GL.glGetString(i[1]).decode()),
File "errorchecker.pyx", line 53, in OpenGL_accelerate.errorchecker._ErrorChecker.glCheckError (src\errorchecker.c:1218)
OpenGL.error.GLError: GLError(
err = 1280,
description = b'\x96\xb3\x8c\xf8\x82\xc8\x97\xf1\x8b\x93',
baseOperation = glGetString,
cArguments = (GL_SHADING_LANGUAGE_VERSION,)
)
サンプルプログラムを探す¶
なにぶん知識がないものだから Google で GLSL 等の単語を検索して、色々と漁ってみるしかない。次のようなサイトが取っ掛かりになる。
- GLSL 1.2 Tutorial
バージョンが 1.2 と古いが、参考になった。
- rndblnch / opengl-programmable
話が早いことに PyOpenGL で書かれたプログラムも発見できた。
サンプルコード全体¶
プログラムをスクリプトファイルの形で構成する。全体を以下に示す:
#!/usr/bin/env python
"""shaderdemo.py: Demonstrate GLSL.
References:
* rndblnch / opengl-programmable
<http://bitbucket.org/rndblnch/opengl-programmable>
* OpenGLBook.com
<http://openglbook.com/chapter-4-entering-the-third-dimension.html>
* Tutorials for modern OpenGL (3.3+)
<http://www.opengl-tutorial.org/beginners-tutorials/tutorial-3-matrices/>
"""
# pylint: disable=unused-argument, no-self-use, invalid-name, no-member
import sys
import os
import colorsys
from ctypes import c_void_p
import numpy as np
from PIL import Image
import OpenGL.GL as GL
from modernapp import ModernApp
VERT_SHADER_SOURCE = """
#version 330 core
uniform mat4 camera;
uniform mat4 projection;
uniform mat4 rotation;
layout(location=0) in vec4 in_Position;
layout(location=1) in vec4 in_Color;
layout(location=2) in vec2 in_TexCoord;
out vec4 ex_Color;
out vec2 ex_TexCoord;
void main(void)
{
gl_Position = projection * camera * rotation * in_Position;
ex_Color = in_Color;
ex_TexCoord = in_TexCoord;
}
"""
FRAG_SHADER_SOURCE = """
#version 330 core
uniform sampler2D texture_sampler;
in vec4 ex_Color;
in vec2 ex_TexCoord;
out vec4 out_Color;
void main(void)
{
out_Color = texture(texture_sampler, ex_TexCoord).rgba;
if(out_Color.a < 0.9){
out_Color = ex_Color;
}
}
"""
class ShaderDemoApp(ModernApp):
"""A GLSL demonstration."""
def __init__(self, **kwargs):
"""Initialize an instance of class ShaderDemoApp."""
kwargs.setdefault('context_version', (3, 1))
super(ShaderDemoApp, self).__init__(**kwargs)
self.buffer_id = 0
self.index_buffer_id = 0
self.num_triangles = 24
self.texture = 0
self.vao_id = 0
def get_shader_sources(self):
"""Initialize shader source."""
return {
GL.GL_VERTEX_SHADER: VERT_SHADER_SOURCE,
GL.GL_FRAGMENT_SHADER: FRAG_SHADER_SOURCE,}
def init_object(self):
"""Initialize the object to be displayed."""
num_triangles = self.num_triangles
vertices = np.array(
[0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0])
for i in range(num_triangles):
theta = 2 * np.pi * i / num_triangles
pos = np.array([np.cos(theta), np.sin(theta), 1.0, 1.0])
color = colorsys.hsv_to_rgb(i / num_triangles, 1.0, 1.0)
tex = pos[0:2]
vertices = np.hstack((vertices, pos, color, [1.0], tex))
vertices = vertices.astype(np.float32)
indices = np.hstack(
(range(num_triangles + 1), 1)).astype(np.uint8)
self.vao_id = GL.glGenVertexArrays(1)
GL.glBindVertexArray(self.vao_id)
# Create buffer objects data.
self.buffer_id = GL.glGenBuffers(1)
GL.glBindBuffer(
GL.GL_ARRAY_BUFFER, self.buffer_id)
GL.glBufferData(
GL.GL_ARRAY_BUFFER, vertices, GL.GL_STATIC_DRAW)
self.index_buffer_id = GL.glGenBuffers(1)
GL.glBindBuffer(
GL.GL_ELEMENT_ARRAY_BUFFER, self.index_buffer_id)
GL.glBufferData(
GL.GL_ELEMENT_ARRAY_BUFFER, indices, GL.GL_STATIC_DRAW)
# xyzwrgbauv := [float32] * 4 + [float32] * 4 + [float32] * 2
vsize = np.nbytes[np.float32] * 4
csize = np.nbytes[np.float32] * 4
tsize = np.nbytes[np.float32] * 2
unit_size = vsize + csize + tsize
GL.glVertexAttribPointer(
0, 4, GL.GL_FLOAT, GL.GL_FALSE, unit_size, None)
GL.glVertexAttribPointer(
1, 4, GL.GL_FLOAT, GL.GL_FALSE, unit_size, c_void_p(vsize))
if self.texture:
GL.glVertexAttribPointer(
2, 2, GL.GL_FLOAT, GL.GL_FALSE,
unit_size, c_void_p(vsize + csize))
GL.glEnableVertexAttribArray(0)
GL.glEnableVertexAttribArray(1)
if self.texture:
GL.glEnableVertexAttribArray(2)
def init_texture(self):
"""Initialize textures."""
source_path = os.path.join(
os.path.dirname(__file__), '../../_images/illvelo.png')
img = Image.open(source_path).resize((256, 256))
assert img.mode == 'RGBA'
tex_id = GL.glGetUniformLocation(
self.program_manager.program_id, b"texture_sampler")
GL.glUniform1i(tex_id, 0)
GL.glActiveTexture(GL.GL_TEXTURE0)
self.texture = GL.glGenTextures(1)
GL.glBindTexture(GL.GL_TEXTURE_2D, self.texture)
GL.glTexImage2D(
GL.GL_TEXTURE_2D, 0, GL.GL_RGBA,
img.size[0], img.size[1],
0, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, img.tobytes())
GL.glTexParameterf(
GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL.GL_REPEAT)
GL.glTexParameterf(
GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_REPEAT)
GL.glTexParameterf(
GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR)
GL.glTexParameterf(
GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR)
def do_render(self):
"""Render the object."""
GL.glDrawElements(
GL.GL_TRIANGLE_FAN, self.num_triangles + 2,
GL.GL_UNSIGNED_BYTE, None)
def cleanup(self):
"""The clean up callback function."""
super(ShaderDemoApp, self).cleanup()
self.destroy_vbo()
GL.glDeleteTextures(1, [self.texture])
def destroy_vbo(self):
"""Clean up VAO and VBO."""
if self.texture:
GL.glDisableVertexAttribArray(2)
GL.glDisableVertexAttribArray(1)
GL.glDisableVertexAttribArray(0)
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, 0)
GL.glDeleteBuffers(1, [self.buffer_id])
GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, 0)
GL.glDeleteBuffers(1, [self.index_buffer_id])
GL.glBindVertexArray(0)
GL.glDeleteVertexArrays(1, [self.vao_id])
def main(args):
"""The main function."""
app = ShaderDemoApp(
window_title=b'Shader Demo',
window_size=(300, 300),
camera_eye=(2., 2., 2.),
camera_center=(0., 0., 0.),
camera_up=(0., 0., 1.))
app.run(sys.argv)
if __name__ == "__main__":
sys.exit(main(sys.argv))
これはイルベロの描かれた正多角形を描画するだけのプログラムである。シェーダープログラム、テクスチャー、座標変換機能はベースクラスで実装済みである。残念ながら、照光処理は実装していない。
ポイント解説¶
本節では上記コードを要所に絞って解説していく。解説するポイントの順序は、スクリプトの先頭から末尾に向かってとする。
インポート¶
import os
import colorsys
from ctypes import c_void_p
import numpy as np
from PIL import Image
import OpenGL.GL as GL
from modernapp import ModernApp
頂点の RGB を指定するのを間接的に行う予定で、標準モジュールの
colorsys
をインポートする。後述するバッファーデータのオフセット指定のために、どうしても謎のクラス
ctypes.c_void_p
をインポートする必要がある。OpenGL 3.1 を志向しているので、GLU は決してインポートしない。
クラス
ModenApp
のサブクラスをつくるため、当然インポートする。
シェーダーコード¶
シェーダーコード自身については、OpenGL でも PyOpenGL でも何ら変わらない。Python の場合はトリプルクォート記法が便利だ。
頂点シェーダーコード¶
#version 330 core
uniform mat4 camera;
uniform mat4 projection;
uniform mat4 rotation;
layout(location=0) in vec4 in_Position;
layout(location=1) in vec4 in_Color;
layout(location=2) in vec2 in_TexCoord;
out vec4 ex_Color;
out vec2 ex_TexCoord;
void main(void)
{
gl_Position = projection * camera * rotation * in_Position;
ex_Color = in_Color;
ex_TexCoord = in_TexCoord;
}
OpenGL コンテキストバージョンを 3.1 以上に上げると、シェーダーコードの先頭行を
#version 330
のようにバージョンの宣言をするのがほぼ必須となる。明示的に指定しないと、コンパイラーはかなり古いシェーダーコードとみなす。結果、コンパイルエラーを引き起こす。layout(location=0) in vec4 ...
の行の宣言により、Python コードからシェーダーに何らかの四次元ベクトルデータを引き渡すこと示す。関数呼び出し
glVertexAttribPointer(0, 4, ...)
で、頂点シェーダー内location = 0
のデータに四次元ベクトルデータをセットする。関数呼び出し
glEnableVertexAttribArray(0)
で、このやりとりを有効にするという手続きになるのだろう。
フラグメントシェーダーコード¶
#version 330 core
uniform sampler2D texture_sampler;
in vec4 ex_Color;
in vec2 ex_TexCoord;
out vec4 out_Color;
void main(void)
{
out_Color = texture(texture_sampler, ex_TexCoord).rgba;
if(out_Color.a < 0.9){
out_Color = ex_Color;
}
}
頂点シェーダーの出力がフラグメントシェーダーの入力となる。
最新バージョンの GLSL では
gl_FragColor
が廃止されたらしいので、本シェーダーの出力をout ...
の行で別途宣言する。
クラス ShaderDemoApp
¶
OpenGL 3.0 以降準拠の PyOpenGL スクリプトのためのベースクラス ModenApp
のサブクラスを定義する。
メソッド __init__
¶
"""Initialize an instance of class ShaderDemoApp."""
kwargs.setdefault('context_version', (3, 1))
super(ShaderDemoApp, self).__init__(**kwargs)
self.buffer_id = 0
self.index_buffer_id = 0
self.num_triangles = 24
self.texture = 0
self.vao_id = 0
コンテキストバージョンは、コンストラクターに明示的に指示のない限り、本稿の意図通りに 3.1 とする。
メンバーデータ
buffer_id
およびindex_buffer_id
を、関数glGenBuffers
系の ID を管理するために宣言する。プログラムがそれらの生成時と削除時に参照する。メンバーデータ
num_triangles
は描画する多角形の頂点数を示すものだ。扱いとしては定数である。メンバーデータ
texture
は関数glGenTextures
の戻り値をキープしておくためのものだ。メンバーデータ
vao_id
は頂点配列オブジェクト用だ。
メソッド get_shader_sources
¶
"""Initialize shader source."""
return {
GL.GL_VERTEX_SHADER: VERT_SHADER_SOURCE,
GL.GL_FRAGMENT_SHADER: FRAG_SHADER_SOURCE,}
メソッド get_shader_source
は前述のシェーダープログラム初期化メソッドから呼び出すためのテンプレートメソッドとする。頂点シェーダーやフラグメントシェーダーのソースコードをメンバーデータの辞書に設定する。
メソッド init_object
¶
"""Initialize the object to be displayed."""
num_triangles = self.num_triangles
vertices = np.array(
[0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0])
for i in range(num_triangles):
theta = 2 * np.pi * i / num_triangles
pos = np.array([np.cos(theta), np.sin(theta), 1.0, 1.0])
color = colorsys.hsv_to_rgb(i / num_triangles, 1.0, 1.0)
tex = pos[0:2]
vertices = np.hstack((vertices, pos, color, [1.0], tex))
vertices = vertices.astype(np.float32)
indices = np.hstack(
(range(num_triangles + 1), 1)).astype(np.uint8)
self.vao_id = GL.glGenVertexArrays(1)
GL.glBindVertexArray(self.vao_id)
# Create buffer objects data.
self.buffer_id = GL.glGenBuffers(1)
GL.glBindBuffer(
GL.GL_ARRAY_BUFFER, self.buffer_id)
GL.glBufferData(
GL.GL_ARRAY_BUFFER, vertices, GL.GL_STATIC_DRAW)
self.index_buffer_id = GL.glGenBuffers(1)
GL.glBindBuffer(
GL.GL_ELEMENT_ARRAY_BUFFER, self.index_buffer_id)
GL.glBufferData(
GL.GL_ELEMENT_ARRAY_BUFFER, indices, GL.GL_STATIC_DRAW)
# xyzwrgbauv := [float32] * 4 + [float32] * 4 + [float32] * 2
vsize = np.nbytes[np.float32] * 4
csize = np.nbytes[np.float32] * 4
tsize = np.nbytes[np.float32] * 2
unit_size = vsize + csize + tsize
GL.glVertexAttribPointer(
0, 4, GL.GL_FLOAT, GL.GL_FALSE, unit_size, None)
GL.glVertexAttribPointer(
1, 4, GL.GL_FLOAT, GL.GL_FALSE, unit_size, c_void_p(vsize))
if self.texture:
GL.glVertexAttribPointer(
2, 2, GL.GL_FLOAT, GL.GL_FALSE,
unit_size, c_void_p(vsize + csize))
GL.glEnableVertexAttribArray(0)
GL.glEnableVertexAttribArray(1)
if self.texture:
GL.glEnableVertexAttribArray(2)
主に頂点バッファーオブジェクトを定義する。
次の段階を踏んでバッファーオブジェクトを構築していく。
NumPy の配列を活用して、描画するべき頂点の各種属性を定義する。
定義した頂点データのアドレス的なものを PyOpenGL を用いて指定する。
まずは本コードで想定している vertices
の「メモリレイアウト」について説明する。
例えば三角形を N 個描くものとする。このとき、座標と色のペアが N 個あることになる。サンプルコードにおける想定はこのようなものだ。
点の座標と色の表現をそれぞれ
(x, y, z, w)
および(R, G, B, A)
で表現することにした。そして、一つの頂点データを(x, y, z, w, R, G, B, A)
で表現することにした。点・色の各成分の型を OpenGL の
GL_FLOAT
に相当する、NumPy のnp.float32
で表現することにした。頂点データ全体を
(x0, y0, z0, w0, R0, G0, B0, A0, ..., )
のように直列して表現することにした。
結局は GL_FLOAT
型の 4 * 8 * N 要素からなる配列を作成することになるが、プログラムでは
np.array
を配列バッファーとして、np.array.hstack
を配列の単純連結操作として、np.array.astype
を高精度の型 (e.g.float64
) から低精度の型 (e.g.float32
) へキャストするための操作として
適宜応用した。
描画するイメージ自体についてのプランはこうだ。
平面 z = 1 上の単位円周上に正 24 角形を描く。
GL_TRIANGLE_FAN
で原点(0, 0)
を中心としてファンとして描く。頂点の色は配列のインデックスをベースに HSV で決める。中心は白にしておく。
頂点配列オブジェクトを新たに導入する。関数
glGenVertexArrays
,glBindVertexArray
を用いる。関数
glGenBuffers
での実引数に注意。1 の場合はリストではなく、単なる整数が戻る。関数
glBufferData
の第二引数には NumPy の配列オブジェクトを直接指定できる。便利。旧式の関数
glVertexPointer
,glColorPointer
に代え、現代的なglVertexAttribPointer
を用いる。細かい説明は PyOpenGL プログラムに頻出する技法・注意点 の
ctypes
の説明を参照。以前メソッド
do_render
で呼び出していたglEnableClientState
の代わるものとして、関数glEnableVertexAttribArray
を導入する。
書かずもがなだが、本コードでは VBO を動的に切り替えるようなことはしないため、
VertexAttrib 系関数の呼び出しを初期化メソッドで済ませている。仮に複数種類の VAO
なり VBO なりを用意して、何か動的に切り替えるのであれば、バインド関数の呼び出しを伴った上で、VertexAttrib 系関数の呼び出しをメソッド do_render
に配置するかもしれない。
メソッド init_texture
¶
"""Initialize textures."""
source_path = os.path.join(
os.path.dirname(__file__), '../../_images/illvelo.png')
img = Image.open(source_path).resize((256, 256))
assert img.mode == 'RGBA'
tex_id = GL.glGetUniformLocation(
self.program_manager.program_id, b"texture_sampler")
GL.glUniform1i(tex_id, 0)
GL.glActiveTexture(GL.GL_TEXTURE0)
self.texture = GL.glGenTextures(1)
GL.glBindTexture(GL.GL_TEXTURE_2D, self.texture)
GL.glTexImage2D(
GL.GL_TEXTURE_2D, 0, GL.GL_RGBA,
img.size[0], img.size[1],
0, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, img.tobytes())
GL.glTexParameterf(
GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL.GL_REPEAT)
GL.glTexParameterf(
GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_REPEAT)
GL.glTexParameterf(
GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR)
GL.glTexParameterf(
GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR)
前半部では、ファイルシステムにある PNG ファイルからテクスチャーパターンを生成している。これをフラグメントシェーダーの texture_sampler
に送る。
後半部、関数 glTexImage2D
呼び出し以降は従来の OpenGL での手続き様式と同じである。
メソッド do_render
¶
"""Render the object."""
GL.glDrawElements(
GL.GL_TRIANGLE_FAN, self.num_triangles + 2,
GL.GL_UNSIGNED_BYTE, None)
上述のメソッドでがんばったおかげで、関数 glDrawElements
の最後の引数は
None
で済むようになった。
メソッド cleanup
, destroy_vbo
¶
"""The clean up callback function."""
super(ShaderDemoApp, self).cleanup()
self.destroy_vbo()
GL.glDeleteTextures(1, [self.texture])
def destroy_vbo(self):
"""Clean up VAO and VBO."""
if self.texture:
GL.glDisableVertexAttribArray(2)
GL.glDisableVertexAttribArray(1)
GL.glDisableVertexAttribArray(0)
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, 0)
GL.glDeleteBuffers(1, [self.buffer_id])
GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, 0)
GL.glDeleteBuffers(1, [self.index_buffer_id])
GL.glBindVertexArray(0)
GL.glDeleteVertexArrays(1, [self.vao_id])
関数
glDisableVertexAttribArray
により、VBO を効かなくする。関数
glBindBuffer
で0
を指定して VBO を効かなくしてから、関数glDeleteBuffers
により、VBO を片付ける。関数
glBindVertexArray
で0
を指定して VAO を効かなくしてから、関数glDeleteVertexArrays
を用いて VAO を片付ける。関数
glDeleteTextures
でテクスチャーを片付ける。
一点注意。関数 glDeleteXXXs
の第二引数は array-like を要求するので、困ったことに関数 glGenXXXs
で「一個だけ」名前を生成したときには、ここに示したように利用したモノが一個だけでも、無理矢理それを含む array-like オブジェクトを作る必要がある。
関数 main
¶
"""The main function."""
app = ShaderDemoApp(
window_title=b'Shader Demo',
window_size=(300, 300),
camera_eye=(2., 2., 2.),
camera_center=(0., 0., 0.),
camera_up=(0., 0., 1.))
app.run(sys.argv)
このように各種パラメーターを指定して、クラス ShaderDemoApp
のオブジェクトを生成、実行をする。
実行する¶
スクリプトを実行すると、いつものように描画ウィンドウが出現する。それに加えて
Python 関数 print
による情報がコンソールウィンドウに出力する。
表示されるグラフィックは次のようなものだ。マウスドラッグでズームや回転を試すこともできる。