OpenGL覚え書き3 座標系

CGにおける座標系

CGで使われる座標系には次のようなものがあります。

モデリング座標系 各頂点の座標系
ワールド座標系 物体が配置されている座標系
視点座標系 カメラからみた座標系
バイス座標系 長さ2×2で中心を原点とした正方形型の座標系
ウインドウ座標系 ウインドウサイズと同じ縮尺の座標系

モデリングによって頂点座標を生成してからウインドウにグラフィクスを書くまでに次のような変換を行っています。

モデリング変換 モデリング座標系→ワールド座標系
視野変換 ワールド座標系→視点座標系
投影変換 視点座標系→正規化されたデバイスの座標系
ビューポート変換 正規化デバイス座標系→ウインドウ座標系

OpenGLでは4×4の行列を利用してこれら座標変換を行います。

行列の作成

合成変換を計算するための元となる行列を以下のコマンドで作成します。
glLoadIdentity
単位行列を作成します。
glLoadMatrix*
予め用意した配列を引数に指定して行列を生成します。ここでは配列の成分と行列の要素との対応に注意しなければなりません。
行列のi行j列の要素をmijと書くと、用意するべき配列は
mat=[m00,m10,m20,m30,m01,m11,m21,m31,m02,m12,m22,m32,m03,m13,m23,m33]
となります。

変換行列の計算方法

複数の変換を行う場合は頂点ベクトルに変換行列を逐一かけるのではなく、先に合成変換に対応する行列を計算しておいて、その行列を1度だけ頂点ベクトルにかけます。行列の積を計算するには
glMultMatrix*
というコマンドを利用します。("*"にはfloatまたはdoubleを指定。以降同じ。)
例えばxy方向に0.2,0.5平行移動する行列を作成して、モデリング変換行列を計算するには次のような処理を行います。

    T = [1,0,0,0,\
       0,1,0,0,\
       0,0,1,0,\
       0.2,0.5,0,1]
    glLoadIdentity()
    glMultMatrixf(T)

平行移動など特定の変換を施す行列の場合は、行列を自動生成して積を計算してくれるコマンドが用意されています。
glScale*:拡大縮小
引数はxyz拡大率を指定
glTranslate*:平行移動
引数はxyz移動量を指定
glRotate*:回転
角度(°)、回転中心軸方向ベクトルを指定する

上の例と同じ処理を次のように書くこともできます。

    glLoadIdentity()
    glTranslate(0.2,0.5,0.0)

座標変換の指定

変換行列を導きたい座標変換の種類を指定するには
glMatrixMode
を利用します。引数には、GL_MODELVIEW(モデリング変換),GL_PROJECTION(投影変換),GL_TEXTURE(テクスチャ行列)を指定することができます。

座標変換

行列を計算したあとに元の座標で描画すると、各頂点が座標変換されたあとの値で処理されます。(1,0,0)座標をxy方向に2,5移動して描画する処理は次のようになります。

    glMatrixMode(GL_MODELVIEW)
    glLoadIdentity()
    glTranslate(0.2,0.5,0.0)
    glBegin(GL_TRIANGLES);
    glVertex3f(1.0,0.0,0.0)
    #略
    glEnd()

変換行列を作成するコマンドを一通り確認したので、それぞれの変換を行うコマンドについて確認してみたいと思います。

モデリング変換

glMatrixModeにGL_MODELVIEWを指定するとモデリング変換行列を計算することができます。
下の例では、teapot1を原点中心に30°回転させ、xy方向に0.3ずつ移動させたものがteapot2になります。

def draw():
    glClear(GL_COLOR_BUFFER_BIT)
    
    glMatrixMode(GL_MODELVIEW)
    #draw teapot 1
    glLoadIdentity()
    glBegin(GL_LINES);
    glColor3f(0.7,0.7,1.0)
    glVertex2f(-1,0)
    glVertex2f(1,0)
    glVertex2f(0,-1)
    glVertex2f(0,1)    
    glEnd()
    glutWireTeapot(0.2)
    
    #draw teapot 2
    glLoadIdentity()
    glTranslate(0.3,0.3,0.0)
    glRotate(30,0.0,0.0,1.0)
    glBegin(GL_LINES);
    glColor3f(0.7,1.0,0.7)
    glVertex2f(-1,0)
    glVertex2f(1,0)
    glVertex2f(0,-1)
    glVertex2f(0,1)    
    glEnd()
    glutWireTeapot(0.2)
    
    glFlush()

視野変換

デフォルトでは、カメラの位置は原点で、-z方向を向いており、上方向はy軸の正方向です。
ワールド座標系でカメラの位置をz=0.5へ移動する操作は、視点座標系で物体を-0.5平行移動することと同じです。元の座標系から30°回転させる動作は、視点座標系において-30°の回転行列をかけることと等価になります。
視野変換を実現するためには、カメラと逆動作の変換行列をモデルビュー変換として計算すればよいことがわかります。
ワールド座標系で原点からz=0.5に移動してz軸正方向中心に30°回転する例です。

    glMatrixMode(GL_MODELVIEW)
    glLoadIdentity()
    #view transform
    glTranslate(0.0,0.0,-0.5)
    glRotate(-30,0.0,0.0,1.0)
    #model transform
    glTranslate(0.3,0.3,0.0)
    glRotate(30,0.0,0.0,1.0)

    glBegin(GL_LINES);
    glColor3f(0.7,1.0,0.7)
    glVertex2f(-1,0)
    glVertex2f(1,0)
    glVertex2f(0,-1)
    glVertex2f(0,1)    
    glEnd()
    glutWireTeapot(0.2)

GLUでは下のような、視野変換を行うコマンドが用意されています。適切なタイミングでこのコマンドを呼び出すと視野変換を自動的に行ってくれます。
gluLookAt
引数には、カメラの座標、視点方向、カメラの上方向をxyz座標で指定します。

    glLoadIdentity()
    #view transform
    gluLookAt(0.5,0.0,0.0,-1.0,0.0,0.0,0.0,1.0,0.0)

    glTranslate(0.3,0.3,0.0)
    glRotate(30,0.0,0.0,1.0)

    glBegin(GL_LINES);
    glColor3f(0.7,1.0,0.7)
    glVertex2f(-1,0)
    glVertex2f(1,0)
    glVertex2f(0,-1)
    glVertex2f(0,1)    
    glEnd()
    glutWireTeapot(0.2)

投影変換

3次元空間座標を2次元座標に投影する変換を投影変換と呼びます。頂点とカメラの間に矩形のスクリーンを置き、そこに頂点を投影します。頂点の座標を確定させるには、カメラとスクリーンの距離(near)と視線から4辺までの距離(left,right,bottom,top)が定まらなければならないので、引数は5個になります。ただしこのままでは、遠くの頂点も投影してしまいます。そのためクリッピング用にスクリーン上をもう一つ奥に設置して、2つのスクリーンで囲まれた部分の頂点だけ投影します。この領域を確定させるためには奥のスクリーンとカメラの距離(far)が定まれば十分です。結局引数は6個になります。

透視投影

glFrustum
引数は投影を行う領域を定める値left,right,bottom,top,near,farです。
GLUではより使いやすいコマンドが用意されています。
gluPerspective
fovy(仰角),aspect(スクリーンのアスペクト比),near,far

平行投影

2次元平面に平行に投影するためのコマンドは次の通りです。
glOrtho
引数は投影を行う領域を定めるGLdouble型の値left,right,bottom,top,near,farです。
2次元の頂点同士の投影では、頂点のz座標は0なのでnear=-1,far=1と定めることで引数を4つに減らすことができます。そのように計算する別のコマンドも用意されています。
gluOrtho2D
引数はleft,right,bottom,top

投影変換の例

次の例では、カメラは(0.0,0.0,1.0)に位置しており、原点方向を向いています。スクリーンは中心から0.5離れた辺がなす矩形で、クリッピング領域がz=0.5〜0.0の例です。ティーポットはz<0の部分が表示されていません。

    glBegin(GL_LINES);
    glColor3f(1.0,1.0,1.0)
    glVertex2f(-1,0)
    glVertex2f(1,0)
    glVertex2f(0,-1)
    glVertex2f(0,1)    
    glEnd()
    #projection 
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    glFrustum(-0.5,0.5,-0.5,0.5,0.5,1.0)    
    
    glMatrixMode(GL_MODELVIEW)
    glLoadIdentity()

    #view transform
    gluLookAt(0.0,0.0,1.0,0.0,0.0,-1.0,0.0,1.0,0.0)

    glTranslate(0.3,0.3,0.0)
    glBegin(GL_LINES);
    glColor3f(0.7,1.0,0.7)
    glVertex2f(-1,0)
    glVertex2f(1,0)
    glVertex2f(0,-1)
    glVertex2f(0,1)    
    glEnd()
    glutWireTeapot(0.2)

ビューポート変換

透視変換後は幅と高さが中心から1.0に正規化されています。このデバイス座標を、左下端を中心としてウインドウの1pixと同じ単位のウインドウ座標系に変換する処理をビューポート変換と呼びます。ビューポート変換を行うコマンドは
glViewport
です。引数には、ビューポート部分の頂点座標(ウインドウ座標なので左下端)、幅と高さを指定します。
ウインドウサイズが400×400で、(0,0,200,200)の領域をビューポートにした例です。

from OpenGL.GL import *
from OpenGL.GLUT import *
from OpenGL.GLU import *

def draw():
    glClear(GL_COLOR_BUFFER_BIT)
    
    #draw viewport
    glColor3f(0.0,0.0,1.0)    
    glRectf(-200,-200,0,0)
    #viewport transform
    glViewport(0,0,200,200)

    #draw x,y axis(device coordinate)
    glBegin(GL_LINES);
    glColor3f(1.0,1.0,1.0)
    glVertex2f(-1,0)
    glVertex2f(1,0)
    glVertex2f(0,-1)
    glVertex2f(0,1)    
    glEnd()

    #projection 
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    glFrustum(-0.5,0.5,-0.5,0.5,1.0,2.0)

    glMatrixMode(GL_MODELVIEW)
    glLoadIdentity()
    
    #view transform
    gluLookAt(1.0,0.0,2.0,0.0,0.0,-1.0,0.0,1.0,0.0)
 
    #modeling transform       
    glTranslate(0.3,0.3,0.0)

    glBegin(GL_LINES);
    glColor3f(0.7,1.0,0.7)
    glVertex2f(-1,0)
    glVertex2f(1,0)
    glVertex2f(0,-1)
    glVertex2f(0,1)    
    glEnd()
    glutWireTeapot(0.2)
    glFlush()
    
def main():
    glutInit(sys.argv)
    glutInitDisplayMode(GLUT_RGBA  | GLUT_DEPTH)
    glutInitWindowSize(400, 400)
    glutInitWindowPosition(0, 0)
    glutCreateWindow("PyOpenGL")
    glutDisplayFunc(draw)   
    glClearColor(0.0,0.0,0.0,0.0)
  
    glutMainLoop()

main()