立方体を回転させるサンプル (OpenGL、Python)
[最終更新] (2019/06/03 00:19:45)
最近の投稿
注目の記事

概要

立方体を二つ配置して回転させてみます。ライブラリを用いずに OpenGL API を直接利用します。描画部分のみを IPython で検証するためのソースコードはこちらです。

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 変数で設定します。

$$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) へ変換します。正規化デバイス座標系はクリッピング座標系ともよびます。

  • 視点座標系において切り出す空間を視体積 (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)

直行投影

$$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} $$

ビューポート変換 (正規化デバイス座標系 → デバイス座標系) 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')

プログラムオブジェクトの変数設定およびリンク glBindAttribLocationglBindFragDataLocationglLinkProgram

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

この続きが気になる方は

立方体を回転させるサンプル (OpenGL、Python)

残り文字数は全体の約 32 %
tybot
100 円
関連ページ
    概要 カメラキャリブレーション (Camera Calibration, Camera Resectioning) を行うと、レンズの歪みを表現するパラメータや、カメラのワールド座標系での位置姿勢を推定できます。 チェスボードのようなキャリブレーション専用のボードが利用されます。 キャリブレーションで得られたパラメータを用いると、例えば歪みを補正することができます。