立方体を二つ配置して回転させてみます。ライブラリを用いずに OpenGL API を直接利用します。
wget https://gist.githubusercontent.com/harubot/df886254396a449038ee542ed317f7b3/raw/92216e02d0210b9d81770562ddf7741339f1b286/opengl-setup2.py
DISPLAY=:0 python opengl-setup2.py
立方体の頂点バッファ (ローカル座標系)
立方体の頂点情報を CPU で用意して GPU の頂点バッファに送ります。座標は同次座標系で表現すると行列による演算が簡単になります。
頂点配列オブジェクトの作成 glGenVertexArrays
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
glBindVertexArray = loadGl('glBindVertexArray', None, GLuint)
glBindVertexArray(vaoList[0])
if not glGetError() == GL_NO_ERROR:
raise Exception('glBindVertexArray failed')
頂点バッファオブジェクトの作成 glGenBuffers
立方体を描く場合、頂点バッファを節約するために以下のように二つ用意するとよいです。同じ GL_ARRAY_BUFFER
は複数回 GL_ELEMENT_ARRAY_BUFFER
で参照されます。
- 8 つの頂点座標を格納する頂点バッファ
GL_ARRAY_BUFFER
- 8 つの頂点座標の組み合わせを格納する頂点バッファ
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]
頂点バッファオブジェクトのバインド glBindBuffer
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')
頂点バッファオブジェクトにデータを設定 glBufferData
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')
頂点バッファオブジェクトと頂点属性の対応関係を設定 glVertexAttribPointer
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
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 変数で設定します。
ビュー変換 (ワールド座標系 → 視点座標系)
OpenGL で二次元の画面に表示するにあたり、シーンにおけるカメラの視点が必要になります。カメラから見たときの視点座標系への変換をビュー変換とよびます。モデル変換とまとめてモデルビュー変換ともよびます。
ワールド座標系の座標を z 軸まわりに 45 度回転させて更に z 軸方向に -1 だけ平行移動したものを視点座標系とすると以下のような同次変換行列になります。OpenGL のバーテックスシェーダに uniform 変数で設定します。
投影変換 (視点座標系 → 正規化デバイス座標系)
視点座標系において最終的に画面に表示したい空間を切り出して、x,y,z
が [-1,1]
の立方体内に収まるように新たな正規化デバイス座標系 (Normalized Device Coordinate; NDC) へ変換します。正規化デバイス座標系はクリッピング座標系ともよびます。
- 視点座標系において切り出す空間を視体積 (View Volume) とよびます。
- 正規化デバイス座標系における
[-1,1]
の立方体を標準視体積 (クリッピング領域、クリッピング空間) とよびます。
クリッピング領域は立方体になっており z 方向の深度があります。クリッピング領域は次のステージで xy 平面に投影されます。投影される対象となるクリッピング領域内に収めるために投影変換を行います。投影変換には複数の種類があります。
- 直行投影 (Orthogonal Projection) → 視体積が直方体、遠くも近くも同じ大きさになるように変換します。
- 透視投影 (Perspective Projection) → 視体積が四角錐台、遠くのものが小さくなるように変換します。
直行投影と透視投影の変換行列は最終的に以下のようになります。OpenGL のバーテックスシェーダに uniform 変数で設定して利用します。
n
視点座標系における視体積の前方面について、z 軸方向の原点からの距離 (near)f
視点座標系における視体積の後方面について、z 軸方向の原点からの距離 (far)r
,l
,t
,b
視点座標系における視錐台の前方面について、辺の xy 座標 (right、left、top、bottom)
直行投影
特に 、、 となる、前方面と後方面が正方形で中心が z 軸上の場合を考えると以下のようになります。
透視投影
透視投影における視体積を特に視錐台 (View Frustum) とよびます。
特に 、、 となる、前方面と後方面が正方形で中心が z 軸上の場合を考えると以下のようになります。
ビューポート変換 (正規化デバイス座標系 → デバイス座標系) glViewport
ビューポート変換では正規化デバイス座標系のクリッピング空間を切り出して 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
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
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
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、glBindFragDataLocation、glLinkProgram
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')
プログラムをインストール glUseProgram
glUseProgram(program)
if not glGetError() == GL_NO_ERROR:
raise Exception('glUseProgram failed')
uniform 変数の location を取得 glGetUniformLocation
glGetUniformLocation = loadGl('glGetUniformLocation', GLint, GLuint, POINTER(GLchar))
modelLocation = glGetUniformLocation(program, array('model' + '\x00', dtype=GLchar).ctypes.data_as(POINTER(GLchar)))
if not glGetError() == GL_NO_ERROR:
raise Exception('glGetUniformLocation failed')
viewLocation = glGetUniformLocation(program, array('view' + '\x00', dtype=GLchar).ctypes.data_as(POINTER(GLchar)))
if not glGetError() == GL_NO_ERROR:
raise Exception('glGetUniformLocation failed')
projectionLocation = glGetUniformLocation(program, array('projection' + '\x00', dtype=GLchar).ctypes.data_as(POINTER(GLchar)))
if not glGetError() == GL_NO_ERROR:
raise Exception('glGetUniformLocation failed')
uniform 変数の設定および描画 glUniform、glDrawElements
ascontiguousarray
で配列の領域がメモリに連続して確保されることを保証してから glUniform
に配列のポインタを渡します。OpenGL の仕様上、行と列を転置したものを渡す必要があることにも注意します。
描画時は glDrawArrays ではなく glDrawElements を用います。glDrawElements
の第二引数 24 は GL_ELEMENT_ARRAY_BUFFER
頂点バッファの要素数です。
ビュー変換、投影変換
from numpy import ascontiguousarray
from numpy import array
from numpy import eye
from numpy import sin
from numpy import cos
from numpy import pi
view = array([
[cos(pi/4), -sin(pi/4), 0, 0],
[sin(pi/4), cos(pi/4), 0, 0],
[0, 0, 1, -1],
[0, 0, 0, 1]
], dtype=float32)
r = 2.0
n = 0.25
f = 1.75
projection = array([
[1/r, 0, 0, 0],
[0, 1/r, 0, 0],
[0, 0, -2/(f-n), -(f+n)/(f-n)],
[0, 0, 0, 1]
], dtype=float32)
#projection = array([
# [n/r, 0, 0, 0],
# [0, n/r, 0, 0],
# [0, 0, -(f+n)/(f-n), -2*f*n/(f-n)],
# [0, 0, -1, 0]
#], dtype=float32)
glUniformMatrix4fv = loadGl('glUniformMatrix4fv', None, GLint, GLsizei, GLboolean, POINTER(GLfloat))
glUniformMatrix4fv(viewLocation, 1, GL_FALSE, ascontiguousarray(view.T.reshape(16), dtype=float32).ctypes.data_as(POINTER(GLfloat)))
if not glGetError() == GL_NO_ERROR:
raise Exception('glUniformMatrix4fv failed')
glUniformMatrix4fv(projectionLocation, 1, GL_FALSE, ascontiguousarray(projection.T.reshape(16), dtype=float32).ctypes.data_as(POINTER(GLfloat)))
if not glGetError() == GL_NO_ERROR:
raise Exception('glUniformMatrix4fv failed')
モデル変換、描画
GL_LINES = 0x0001
GL_UNSIGNED_INT = 0x1405
glDrawElements = loadGl('glDrawElements', None, GLenum, GLsizei, GLenum, GLvoid_p)
立方体一つ目
model1 = array(eye(4), dtype=float32)
glUniformMatrix4fv(modelLocation, 1, GL_FALSE, ascontiguousarray(model1.T.reshape(16), dtype=float32).ctypes.data_as(POINTER(GLfloat)))
if not glGetError() == GL_NO_ERROR:
raise Exception('glUniformMatrix4fv failed')
glDrawElements(GL_LINES, 24, GL_UNSIGNED_INT, 0)
if not glGetError() == GL_NO_ERROR:
raise Exception('glDrawElements failed')
立方体二つ目
model2 = array([
[1, 0, 0, 1],
[0, cos(pi/4), -sin(pi/4), 0],
[0, sin(pi/4), cos(pi/4), 0],
[0, 0, 0, 1]
], dtype=float32)
glUniformMatrix4fv(modelLocation, 1, GL_FALSE, ascontiguousarray(model2.T.reshape(16), dtype=float32).ctypes.data_as(POINTER(GLfloat)))
if not glGetError() == GL_NO_ERROR:
raise Exception('glUniformMatrix4fv failed')
glDrawElements(GL_LINES, 24, GL_UNSIGNED_INT, 0)
if not glGetError() == GL_NO_ERROR:
raise Exception('glDrawElements failed')
描画結果の確認
glReadBuffer(GL_COLOR_ATTACHMENT0)
if not glGetError() == GL_NO_ERROR:
raise Exception('glReadBuffer failed')
image = zeros((100, 100, 3), dtype=uint8)
glReadPixels(0, 0, 100, 100, GL_RGB, GL_UNSIGNED_BYTE, image.ctypes.data_as(GLvoid_p))
if not glGetError() == GL_NO_ERROR:
raise Exception('glReadPixels failed')
import matplotlib.pyplot as plt
plt.imshow(image)
plt.show()
直行投影
y軸が下を向いているため 45 度の回転が右回りになっています。
透視投影
後方の面が前方の面よりも小さく描画されていることが分かります。
隠面消去 glEnable
ビューポート変換時に二次元平面に投影されるにあたり、描画される順番によっては後方の部分が前方の描画結果を上書きしてしまうことがあります。これを回避するために隠面消去が必要になります。隠面消去の処理の一つにデプスバッファ法 (Zバッファ法) があります。その他の処理法に背面カリングなどがあります。
デプスバッファ法は OpenGL に組込まれており既定では無効になっています。有効化すると新たに深度情報を格納するデプスバッファが用意され、描画したポリゴンの深度情報が格納されていきます。
GL_DEPTH_TEST = 0x0B71
glEnable = loadGl('glEnable', None, GLenum)
glEnable(GL_DEPTH_TEST)
if not glGetError() == GL_NO_ERROR:
raise Exception('glEnable failed')
glClear(GLbitfield(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT))
if not glGetError() == GL_NO_ERROR:
raise Exception('glClear failed')
テクスチャの適用 (参考)
glEnable(GL_TEXTURE_2D)
で有効化してテクスチャを利用できます。
- テクスチャオブジェクトの生成 glGenTextures
- テクスチャオブジェクトのバインド glBindTexture
GL_MAX_TEXTURE_SIZE
を越えていないことを glGetIntegerv で確認- テクスチャ画像の設定 glTexImage2D
- テクスチャパラメータの設定 glTexParameter
- フラグメントシェーダで
texture()
に UV 座標を設定 - サンプラについて glGenSamplers
関連記事
- Python コードスニペット (条件分岐)if-elif-else sample.py #!/usr/bin/python # -*- coding: utf-8 -*- # コメント内であっても、ASCII外の文字が含まれる場合はエンコーディング情報が必須 x = 1 # 一行スタイル if x==0: print 'a' # 参考: and,or,notが使用可能 (&&,||はエラー) elif x==1: p...
- Python コードスニペット (リスト、タプル、ディクショナリ)リスト range 「0から10まで」といった範囲をリスト形式で生成します。 sample.py print range(10) # for(int i=0; i<10; ++i) ← C言語などのfor文と比較 print range(5,10) # for(int i=5; i<10; ++i) print range(5,10,2) # for(int i=5; i<10;...
- ZeroMQ (zmq) の Python サンプルコードZeroMQ を Python から利用する場合のサンプルコードを記載します。 Fixing the World To fix the world, we needed to do two things. One, to solve the general problem of "how to connect any code to any code, anywhere". Two, to wra...
- Matplotlib/SciPy/pandas/NumPy サンプルコードPython で数学的なことを試すときに利用される Matplotlib/SciPy/pandas/NumPy についてサンプルコードを記載します。 Matplotlib SciPy pandas [NumPy](https://www.numpy
- pytest の基本的な使い方pytest の基本的な使い方を記載します。 適宜参照するための公式ドキュメントページ Full pytest documentation API Reference インストール 適当なパッケージ