Chandler@Berlin -15ページ目

Chandler@Berlin

ベルリン在住

gluLookAt implementation in python

gluLookAt matrix の私の python (numpy) での実装と,その test を以下に示す.この実装はわかりやすさを主にしているので matrix 乗算とtranspose が入っているが,もちろんこれらを省略するような実装も可能である.Test の結果,2つの matrix は私が試した限りでは,1.0e-6 以下の誤差で一致する.(Ubuntu 10.04, python 2.6, glx version 1.4)


# Copyright (C) 2010 H. Yamauchi
# under New (3-clause) BSD license

#
# get lookat matrix (gluLookAt compatible matrix) python, numpy
#
# \param[in] _eye eye point
# \param[in] _lookat lookat point
# \param[in] _up up vector
# \return 4x4 gluLookAt matrix
def getLookAtMatrix(_eye, _lookat, _up):
ez = _eye - _lookat
ez = ez / numpy.linalg.norm(ez)

ex = numpy.cross(_up, ez)
ex = ex / numpy.linalg.norm(ex)

ey = numpy.cross(ez, ex)
ey = ey / numpy.linalg.norm(ey)

rmat = numpy.eye(4)
rmat[0][0] = ex[0]
rmat[0][1] = ex[1]
rmat[0][2] = ex[2]

rmat[1][0] = ey[0]
rmat[1][1] = ey[1]
rmat[1][2] = ey[2]

rmat[2][0] = ez[0]
rmat[2][1] = ez[1]
rmat[2][2] = ez[2]

tmat = numpy.eye(4)
tmat[0][3] = -_eye[0]
tmat[1][3] = -_eye[1]
tmat[2][3] = -_eye[2]

# numpy.array * is element-wise multiplication, use dot()
lookatmat = numpy.dot(rmat, tmat).transpose()

return lookatmat


#
# test getLookAtMatrix routine
#
# generate two matrices, glmat by gluLookAt, mat by getLookAtMatrix.
# Then compare them. (To run this test, You need OpenGL bindings and
# also your Camera implementation that provides eye, lookat, up.)
#
def test_gluLookAt_matrix():

GL.glLoadIdentity()

# This is your camera. It tells eye, lookat, up.
[ep, at, up] = gl_camera.get_lookat()
GLU.gluLookAt(ep[0], ep[1], ep[2],
at[0], at[1], at[2],
up[0], up[1], up[2])

# OpenGL gluLookAt matrix
glmat = None
glmat = GL.glGetDoublev(GL.GL_MODELVIEW_MATRIX)

# my matrix
mat = getLookAtMatrix(ep, at, up)

# debug output
print 'glmat'
print glmat
print 'mat'
print mat

# compare glmat and mat
for i in range(4):
for j in range(4):
if(math.fabs(glmat[i][j] - mat[i][j]) > 1.0e-6):
raise StandardError ('matrix does not match.')

gluLookAt matrix
Gimbal lock はなぜ(いつ)起こるのか?


カメラの方向は回転の matrix,移動は translation matrix によって表現される.OpenGL ではカメラの位置と方向の default というものが既定されていて,それを動かして使うからである.カメラは最初原点にあって,Z の負の方向を見ており,Y の正方向を上にしている.回転と移動の matrix はぞれ次のような形をしている.
Chandler@Berlin-TR matrix
ここで,

- x: カメラの x軸,(前回のblogの)図中では right, プログラム中では side
- y: カメラの y軸,(前回のblogの)図中では up, プログラム中では up
- z: カメラの z軸,(前回のblogの)図中では Z, プログラム中では -forward
- e: カメラの位置,(前回のblogの)図中では eye, プログラム中では eyex, eyey, eyez

プログラムでは,eye, lookat,upが与えられるので,

z = normalize(eye - lookat)
x = normalize(cross(up,z))
y = normalize(cross(z, x))

として計算されている.cross は外積であり,normalize はベクトルの正規化の関数を示す.計算が z, x, yの順番になっていることに注意すること.

ところで,この matrix では z が -view の方向であることに気がつかれただろうか.これは OpenGL の depth で言えば depth の負の方向である.つまりカメラの基底座標はカメラのレンズの方向ではなく,後ろを向いているのである.しかもカメラから遠いものほど depth は大きくなるようになっているが,この depth が正のものは,world 座標では負になる.座標系は単なる表現であるので間違いではない.たとえば viewport の座標は Y は正が上であるが,スクリーンでは Y は下向きが正である.この world 座標系とカメラの座標系の違いは私はよく混乱する.


では,R がなぜ回転と呼ばれているのかを説明しよう. x, y,z はそれぞれ直交するように外積で作られている.また,これらは全て正規化されていて長さが 1 のベクトルになっている.これが回転と呼ばれているのは,長さを変えずに,方向だけ変化させているからである.私としては回転として考えることよりも,座標軸の変換として考えた方が直感的な気がする.それは回転の場合には回転軸とか動いている様子とかを私は考えてしまい,それがこの基底ベクトル中には見えにくいからである.R が座標軸を変換するという意味は,standard な基底をこの R に掛けると,以下のようになるからである.
Chandler@Berlin-whyRisRotating

つまり,[1 0 0 0]^T は x に,[0 1 0 0]^T はyに,[0 0 1 0]^T は z に変換されている.良くみると, [2 0 0 0]^T は 2x にと,各軸の長さも保存される.これが座標変換という意味である.そしてこういう変換は長さが変換せずに方向だけ変化するので,実際には剛体を回転することと同じである.

あとはカメラの位置の分だけ移動すれば良い.TR の式に内積が出てきているのは,実際に計算するとわかるだろうが,ある位置を新しい座標で示すというのは,図 2に示すように,各座標軸への投影である.したがってcos の計算がでてくる.つまり内積の登場となる.

$Chandler@Berlin-basisprojection
Figure 2: Basis transformation and dot product

OpenGL の gluLookAt の作る matrix がどういうものか,私なりにソースコードから解釈してみた.このカメラの基底の行列を上手く使うと,カメラをトラックボール上に配置して眺めるような navigation ができる.3D の物体を観測するようなプログラムを書く場合,最初に gimbal lock (ギンバルロック,ジンバルロック)を避けるようなプログラムを書くことになるだろう.gimbal lock は,視線方向が up vectorに近くなることで発生する.これを避けるには,単純にカメラの基底自体を回転し,全ての基底が常に直交するように気を配れば良い.図 3の (b) のように,視線方向のみ変化させるような実装は簡単だが,ある場所でレンズが折れてしまう.これを避けるには,単純に(c)のようにカメラを回転させることである.


$Chandler@Berlin
Figure 3: Gimbal lock camerta. When you want to see a bit upper direction, (b) bending the lens implementation, this causes the gimbal lock, (c) rotate the camera, no gimbal lock

次回はおまけとして python での gluLookAt の実装を紹介する.これは最初に述べたように,OpenGL と自分の OpenGL に依存しない renderer を混合させたい時などに使う.


gluLookAt function


3次元のシーンを描画するために,ゲームなどでも利用されている OpenGL というAPI を使うことがある.OpenGL の世界で閉じていれば,内部で実際にどんなmatrixが生成されているかはあまり気にすることはないが,OpenGL で描画しているシーンに何かを overlay したいなどという時,たとえば,独自の renderer を趣味で書いている人達でかつ OpenGL も使う人達は,OpenGL の内部でどんなmatrixが生成されているか知っていると有用なことがある.

したがってこの話はかなり特殊なものなので,まあ,ここまでの話で興味のない人はまた次の機会にお会いしましょう.

今回は gluLookAt という OpenGL のユーティリティ関数の作る matrix についてである.

gluLookAt は三次元のシーンにカメラを方向を考えて配置するものである.最近,この matrix をどう生成するかということを知る必要があったが,どうも説明している Page がみつからなかった.結局 Mesa の実装の内部を見ることになった.(Mesa-7.5.1/src/glu/sgi/libutil/project.c) このコードはわかりやすい変数名を使っており,コメントも書いてある.以下が Mesa 7.5.1 の該当部分のソースコードである.


/*
* SGI FREE SOFTWARE LICENSE B (Version 2.0, Sept. 18, 2008)
* Copyright (C) 1991-2000 Silicon Graphics, Inc. All Rights Reserved.
*/
void GLAPIENTRY
gluLookAt(GLdouble eyex, GLdouble eyey, GLdouble eyez, GLdouble centerx,
GLdouble centery, GLdouble centerz, GLdouble upx, GLdouble upy,
GLdouble upz)
{
float forward[3], side[3], up[3];
GLfloat m[4][4];

forward[0] = centerx - eyex;
forward[1] = centery - eyey;
forward[2] = centerz - eyez;

up[0] = upx;
up[1] = upy;
up[2] = upz;

normalize(forward);

/* Side = forward x up */
cross(forward, up, side);
normalize(side);

/* Recompute up as: up = side x forward */
cross(side, forward, up);

__gluMakeIdentityf(&m[0][0]);
m[0][0] = side[0];
m[1][0] = side[1];
m[2][0] = side[2];

m[0][1] = up[0];
m[1][1] = up[1];
m[2][1] = up[2];

m[0][2] = -forward[0];
m[1][2] = -forward[1];
m[2][2] = -forward[2];

glMultMatrixf(&m[0][0]);
glTranslated(-eyex, -eyey, -eyez);
}



ところで,OpenGL の座標系や matrix の実装は私にはとても混乱しやすい.実
際,OpenGL の official の page にも,http://www.opengl.org/resources/faq/technical/transformations.htm

Column-major notation suggests that matrices are not laid out in memory
as a programmer would expect.

行列でColumn-major記法を使うという時には,(行列の要素は)プログラマが通常思っているようにはメモリ上に格納されていないことを示唆している.


と,わざわざ思った通りとはまず違うんだよ,と書いてあったりするほどである.


そこで私は最初には数学の通常の表記を使って考え,実装時に解釈することにしている.というわけで,ここでは matrix には数学の通常の記法を使うが,C/C++などで array を使って実装する時には注意されたい.

図1にはカメラを基準にした座標(左)とカメラの座標は他の座標にはよらないことを示している.

カメラは剛体と考えて良いので,できることは回転と移動である.カメラを伸ばしたり縮めたり,あるいは壊すこともできるが,OpenGL のカメラはそういうものではないとする.

Chandler@Berlin-cam1
Chandler@Berlin-cam2
Figure 1: Camera coordinates. Camera coordinates and other coordinates

ここで model view の duality ということに注意しておく.これは

- シーン全体は停止していて,カメラが 1m 前進すること
- カメラが停止していて,シーン全体が 1m 後退すること

では,実はカメラに映るものは変化しない.電車の駅にて,電車に乗り込み発車を待っている時,時に自分が動いたのか相手が動いたのか間違えることがあるが,自分(カメラ)が動いた時に見える相手と,相手が動いた時(シーン全体が動いた)に見える相手が実はほとんど同じ場合がある.この変換を示す matrix をmodel view matrix と言う.model の動きと view の動きを組合せたものである.

今回の話はカメラの位置と方向が作る matrix に関してなので view matrix とでも言うべきだが,OpenGL では上記の model view duality からこれらを一つにまとめている.そこで実装では model view matrix となっているが,ここで話をしているのは view matrix のみである.


次回は gluLookAt の生成する回転と移動を表現する matrix に関して述べよう.