立方体を二つ配置して回転させてみます。ライブラリを用いずに OpenGL API を直接利用します。描画部分のみを IPython で検証するためのソースコードはこちらです。
wget https://gist.githubusercontent.com/harubot/df886254396a449038ee542ed317f7b3/raw/92216e02d0210b9d81770562ddf7741339f1b286/opengl-setup2.py
DISPLAY=:0 python opengl-setup2.py
立方体の頂点情報を CPU で用意して GPU の頂点バッファに送ります。座標は同次座標系で表現すると行列による演算が簡単になります。
vaoList = zeros(1, dtype=GLuint)
glGenVertexArrays = loadGl('glGenVertexArrays', None, GLsizei, POINTER(GLuint))
glGenVertexArrays(1, vaoList.ctypes.data_as(POINTER(GLuint)))
if not glGetError() == GL_NO_ERROR:
raise Exception('glGenVertexArrays failed')
glBindVertexArray = loadGl('glBindVertexArray', None, GLuint)
glBindVertexArray(vaoList[0])
if not glGetError() == GL_NO_ERROR:
raise Exception('glBindVertexArray failed')
立方体を描く場合、頂点バッファを節約するために以下のように二つ用意するとよいです。同じ GL_ARRAY_BUFFER
は複数回 GL_ELEMENT_ARRAY_BUFFER
で参照されます。
GL_ARRAY_BUFFER
GL_ELEMENT_ARRAY_BUFFER
作成時は区別されません。
glGenBuffers = loadGl('glGenBuffers', None, GLsizei, POINTER(GLuint))
buffers = zeros(2, dtype=GLuint)
glGenBuffers(2, buffers.ctypes.data_as(POINTER(GLuint)))
if not glGetError() == GL_NO_ERROR:
raise Exception('glGenBuffers failed')
vbo = buffers[0]
ibo = buffers[1]
GL_ARRAY_BUFFER
としてバインドします。
GL_ARRAY_BUFFER = 0x8892
glBindBuffer = loadGl('glBindBuffer', None, GLenum, GLuint)
glBindBuffer(GL_ARRAY_BUFFER, vbo)
if not glGetError() == GL_NO_ERROR:
raise Exception('glBindBuffer failed')
GL_ARRAY_BUFFER
のデータを設定します。
GLsizeiptr = c_uint
GLvoid_p = c_void_p
GL_STATIC_DRAW = 0x88E4
from numpy import float32
vertices = array([
[-0.5,-0.5,-0.5, 1],
[-0.5,-0.5, 0.5, 1],
[-0.5, 0.5,-0.5, 1],
[-0.5, 0.5, 0.5, 1],
[ 0.5,-0.5,-0.5, 1],
[ 0.5,-0.5, 0.5, 1],
[ 0.5, 0.5,-0.5, 1],
[ 0.5, 0.5, 0.5, 1]
], dtype=float32)
glBufferData = loadGl('glBufferData', None, GLenum, GLsizeiptr, GLvoid_p, GLenum)
glBufferData(GL_ARRAY_BUFFER,
GLsizeiptr(vertices.size * vertices.dtype.itemsize),
vertices.ctypes.data_as(GLvoid_p),
GL_STATIC_DRAW)
if not glGetError() == GL_NO_ERROR:
raise Exception('glBufferData failed')
GL_ARRAY_BUFFER
のデータをバーテックスシェーダの in
変数で参照できるようにします。
GLboolean = c_uint
GL_FLOAT = 0x1406
glVertexAttribPointer = loadGl('glVertexAttribPointer', None, GLuint, GLint, GLenum, GLboolean, GLsizei, GLvoid_p)
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, 0)
if not glGetError() == GL_NO_ERROR:
raise Exception('glVertexAttribPointer failed')
glVertexAttribPointer
の引数について補足
0
position 頂点属性を指定しています。4
glBufferData
で設定した座標データは4次元です。GL_FLOAT
座標データの型です。0
glBufferData
で格納したデータに複数の頂点属性用のデータが入っている場合は変更します。0
glBufferData
で格納したデータに複数の頂点属性用のデータが入っている場合は変更します。glEnableVertexAttribArray = loadGl('glEnableVertexAttribArray', None, GLuint)
glEnableVertexAttribArray(0)
if not glGetError() == GL_NO_ERROR:
raise Exception('glEnableVertexAttribArray failed')
GL_ELEMENT_ARRAY_BUFFER
としてバインド
GL_ELEMENT_ARRAY_BUFFER = 0x8893
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo)
if not glGetError() == GL_NO_ERROR:
raise Exception('glBindBuffer failed')
GL_ELEMENT_ARRAY_BUFFER
のデータ設定
from numpy import uint32
indices = array([
[0,1],
[0,2],
[0,4],
[1,3],
[1,5],
[2,3],
[2,6],
[3,7],
[4,5],
[4,6],
[5,7],
[6,7]
], dtype=uint32)
glBufferData(GL_ELEMENT_ARRAY_BUFFER,
GLsizeiptr(indices.size * indices.dtype.itemsize),
indices.ctypes.data_as(GLvoid_p),
GL_STATIC_DRAW)
if not glGetError() == GL_NO_ERROR:
raise Exception('glBufferData failed')
後述のとおり、描画時は glDrawArrays ではなく glDrawElements を用います。
STL ファイル等で表現されたオブジェクトのメッシュの各頂点は、オブジェクト内のローカル座標系における座標を持ちます。複数のオブジェクトを同じ環境に配置する場合、配置される環境のワールド座標系における、オブジェクトのメッシュの頂点の座標が必要になります。ローカル座標系からワールド座標系への変換をモデル変換とよびます。環境のことをシーンともよびます。
同次変換行列を立方体二つについてそれぞれ以下のように設定することにします。一つ目はローカル座標系の座標をそのままワールド座標系でも用います。二つ目はローカル座標系の座標を x 軸まわりに 45 度回転して更に x 軸方向に 1.0 だけ平行移動します。これらの行列は OpenGL のバーテックスシェーダに uniform 変数で設定します。
$$M_1 = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix} $$
$$M_2 = \begin{pmatrix} 1 & 0 & 0 & 1 \\ 0 & \cos \frac{\pi}{4} & -\sin \frac{\pi}{4} & 0 \\ 0 & \sin \frac{\pi}{4} & \cos \frac{\pi}{4} & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix} $$
OpenGL で二次元の画面に表示するにあたり、シーンにおけるカメラの視点が必要になります。カメラから見たときの視点座標系への変換をビュー変換とよびます。モデル変換とまとめてモデルビュー変換ともよびます。
ワールド座標系の座標を z 軸まわりに 45 度回転させて更に z 軸方向に -1 だけ平行移動したものを視点座標系とすると以下のような同次変換行列になります。OpenGL のバーテックスシェーダに uniform 変数で設定します。
$$V = \begin{pmatrix} \cos \frac{\pi}{4} & -\sin \frac{\pi}{4} & 0 & 0 \\ \sin \frac{\pi}{4} & \cos \frac{\pi}{4} & 0 & 0 \\ 0 & 0 & 1 & -1 \\ 0 & 0 & 0 & 1 \end{pmatrix} $$
視点座標系において最終的に画面に表示したい空間を切り出して、x,y,z
が [-1,1]
の立方体内に収まるように新たな正規化デバイス座標系 (Normalized Device Coordinate; NDC) へ変換します。正規化デバイス座標系はクリッピング座標系ともよびます。
[-1,1]
の立方体を標準視体積 (クリッピング領域、クリッピング空間) とよびます。クリッピング領域は立方体になっており z 方向の深度があります。クリッピング領域は次のステージで xy 平面に投影されます。投影される対象となるクリッピング領域内に収めるために投影変換を行います。投影変換には複数の種類があります。
直行投影と透視投影の変換行列は最終的に以下のようになります。OpenGL のバーテックスシェーダに uniform 変数で設定して利用します。
n
視点座標系における視体積の前方面について、z 軸方向の原点からの距離 (near)f
視点座標系における視体積の後方面について、z 軸方向の原点からの距離 (far)r
,l
,t
,b
視点座標系における視錐台の前方面について、辺の xy 座標 (right、left、top、bottom)$$P_1 = \begin{pmatrix} \frac{2}{r - l} & 0 & 0 & -\frac{r + l}{r - l} \\ 0 & \frac{2}{t - b} & 0 & -\frac{t + b}{t - b} \\ 0 & 0 & -\frac{2}{f - n} & -\frac{f+n}{f - n} \\ 0 & 0 & 0 & 1 \end{pmatrix} $$
特に $r+l=0$、$t+b=0$、$r=t$ となる、前方面と後方面が正方形で中心が z 軸上の場合を考えると以下のようになります。
$$P_1 = \begin{pmatrix} \frac{1}{r} & 0 & 0 & 0 \\ 0 & \frac{1}{r} & 0 & 0 \\ 0 & 0 & -\frac{2}{f - n} & -\frac{f+n}{f - n} \\ 0 & 0 & 0 & 1 \end{pmatrix} $$
透視投影における視体積を特に視錐台 (View Frustum) とよびます。
$$P_2 = \begin{pmatrix} \frac{2n}{r - l} & 0 & \frac{r + l}{r - l} & 0 \\ 0 & \frac{2n}{t - b} & \frac{t + b}{t - b} & 0 \\ 0 & 0 & -\frac{f + n}{f - n} & -\frac{2fn}{f - n} \\ 0 & 0 & -1 & 0 \end{pmatrix} $$
特に $r+l=0$、$t+b=0$、$r=t$ となる、前方面と後方面が正方形で中心が z 軸上の場合を考えると以下のようになります。
$$P_2 = \begin{pmatrix} \frac{n}{r} & 0 & 0 & 0 \\ 0 & \frac{n}{r} & 0 & 0 \\ 0 & 0 & -\frac{f + n}{f - n} & -\frac{2fn}{f - n} \\ 0 & 0 & -1 & 0 \end{pmatrix} $$
ビューポート変換では正規化デバイス座標系のクリッピング空間を切り出して xy 平面のビューポートに投影します。ビューポートのサイズは glViewport で指定します。xy 平面内における座標系をデバイス座標系とよびます。
glViewport = loadGl('glViewport', None, GLint, GLint, GLsizei, GLsizei)
glViewport(0, 0, 100, 100)
if not glGetError() == GL_NO_ERROR:
raise Exception('glViewport failed')
glViewport
の引数について
0
,0
描画するカラーバッファについて、描画範囲となる矩形の左下の座標です。100
,100
描画するカラーバッファについて、描画範囲となる矩形の幅と高さです。第三引数と第四引数の値が異なる場合はビューポート変換によってアスペクト比が変化します。投影変換時に画面のアスペクト比を考慮しておくと、ビューポート変換でデバイスの画面に描画されたときにも縦横の比が保たれます。
glShaderSource
でシェーダオブジェクトにソースコードを設定します。
vstring = """#version 130
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
in vec4 position;
void main() {
gl_Position = projection * view * model * position;
}
"""
glShaderSource(vobj, 1,
byref(array(vstring, dtype=GLchar).ctypes.data_as(POINTER(GLchar))),
byref(GLint(len(vstring))))
if not glGetError() == GL_NO_ERROR:
raise Exception('glShaderSource failed')
fstring = """#version 130
out vec4 fragment;
void main() {
fragment = vec4(0.0, 1.0, 0.0, 1.0);
}
"""
glShaderSource(fobj, 1,
byref(array(fstring, dtype=GLchar).ctypes.data_as(POINTER(GLchar))),
byref(GLint(len(fstring))))
if not glGetError() == GL_NO_ERROR:
raise Exception('glShaderSource failed')
glCompileShader(vobj)
if not glGetError() == GL_NO_ERROR:
raise Exception('glCompileShader failed')
glCompileShader(fobj)
if not glGetError() == GL_NO_ERROR:
raise Exception('glCompileShader failed')
コンパイルが成功したかどうかの確認
params = GLint(0)
glGetShaderiv(GLuint(vobj), GL_COMPILE_STATUS, byref(params))
if not glGetError() == GL_NO_ERROR:
raise Exception('glGetShaderiv failed')
if not params.value == GL_TRUE:
raise Exception('compilation failed')
params = GLint(0)
glGetShaderiv(GLuint(fobj), GL_COMPILE_STATUS, byref(params))
if not glGetError() == GL_NO_ERROR:
raise Exception('glGetShaderiv failed')
if not params.value == GL_TRUE:
raise Exception('compilation failed')
glAttachShader(program, vobj)
if not glGetError() == GL_NO_ERROR:
raise Exception('glAttachShader failed')
glAttachShader(program, fobj)
if not glGetError() == GL_NO_ERROR:
raise Exception('glAttachShader failed')
シェーダオブジェクトの削除フラグを設定 glDeleteShader
glDeleteShader(vobj)
if not glGetError() == GL_NO_ERROR:
raise Exception('glDeleteShader failed')
glDeleteShader(fobj)
if not glGetError() == GL_NO_ERROR:
raise Exception('glDeleteShader failed')
glBindAttribLocation(program, 0, array('position', dtype=GLchar).ctypes.data_as(POINTER(GLchar)))
if not glGetError() == GL_NO_ERROR:
raise Exception('glBindAttribLocation failed')
glBindFragDataLocation(program, 0, array('fragment', dtype=GLchar).ctypes.data_as(POINTER(GLchar)))
if not glGetError() == GL_NO_ERROR:
raise Exception('glBindFragDataLocation failed')
glLinkProgram(program)
if not glGetError() == GL_NO_ERROR:
raise Exception('glLinkProgram failed')