立方体を回転させるサンプル (OpenGL、Python)
[History] [Last Modified] (2019/05/17 17:32:15)
Recent posts
What is this site?
A platform for makers to share their knowledge.

Share your robots/products with others.
New robots/products

概要

立方体を二つ配置して回転させてみます。ライブラリを用いずに 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

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 変数の設定および描画 glUniformglDrawElements

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 度の回転が右回りになっています。

Uploaded Image

透視投影

後方の面が前方の面よりも小さく描画されていることが分かります。

Uploaded Image

隠面消去 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) で有効化してテクスチャを利用できます。

  1. テクスチャオブジェクトの生成 glGenTextures
  2. テクスチャオブジェクトのバインド glBindTexture
  3. GL_MAX_TEXTURE_SIZE を越えていないことを glGetIntegerv で確認
  4. テクスチャ画像の設定 glTexImage2D
  5. テクスチャパラメータの設定 glTexParameter
  6. フラグメントシェーダで texture() に UV 座標を設定
  7. サンプラについて glGenSamplers
Related pages