同次変換の利用と3次元
Last update: <2004/03/13 17:31:20 +0900>
オブジェクトの位置や姿勢を3次元的に定義するためには行列が便利です.
OpenGLではオブジェクトの移動や回転で行列の概念を利用しています.OpenGLをVRに用いる場合には,視点やオブジェクトの座標変換が不可欠であるといってよいでしょう.
ここでは行列の計算とOpenGLの関わりについて説明します*9.
OpenGLでは右手系の空間座標系を利用していることは前回に述べた通りです.空間座標系とは(x,y,z)の3次元で張られた座標系です.この空間座標系に物体を描画したり,動作させたりするわけですが,最も基準となる座標系のことをワールド座標系と呼びます.
物体(オブジェクト)を配置する最も簡単な方法は,ワールド座標系での座標値を指定して,その位置に物体の点やポリゴンを表示させることです.しかし,点やポリゴンを動かしてアニメーションをする場合,動きより新しい座標値を毎回計算し,更新しなければなりません.これはとても面倒なことです.(下図の右)

図:ワールド座標系とモデル座標系
そこでOpenGLでは座標変換という概念を使います.上の図にあるように,モデル座標系という座標系を用意して,その座標系で物体の座標値を指定しておき,モデル座標系を丸ごと動かす(座標変換する)ことによって,容易に物体の操作を行うことができます.
座標変換操作を数学的にあらわすには,同次変換行列が強力です.次節より同次変換行列を使用した座標変換について述べていきます.
同次変換行列は同次変換行列は4×4の行列で,一般にロボットアームの位置姿勢を表現するのに用いられています.ここでは簡単に行列について説明しますが,詳しくはロボット工学の授業を参考にして下さい.
そもそも座標変換というのは,ある座標系から他の座標系に射影される関係のことを表しています.例えば下の図の例では,モデル座標系で単位立方体の座標値が指定されています.(1,0,0),(1,0,1),(0,0,1),...という数字です.この座標値はモデル座標系でしか通用しません.ワールド座標系で通用させるためには,ワールド座標系から見たモデル座標系の位置関係が必要となります.ワールド座標系からみて,モデル座標系がx軸に100ずれている場合だと,単位立方体の位置はワールド座標系で(101,0.0),(101,0,1),(100,0,1),...となります.
ここでは単なる足し算ですが,この変換操作を座標変換といいます.同次変換行列を使うと次のようにあらわすことができます.
x_m,y_m,z_mがモデル座標系のローカルの位置,x_w,y_w,z_wがワールド座標系での位置になります.
真ん中の4×4の行列が同次変換行列で,ワールド座標系から見てモデル座標系が100ずれていることを意味しています.つまり,この行列はワールド座標系からモデル座標系にx軸方向に+100ずらすという性質を持っています.ただし,座標値の計算はモデル座標ローカル値より,ワールド値に変換していることに注意して下さい.
この同次変換行列を使った基本操作を列挙しておきます.
- 平行移動
基準となる座標系から(x,y,z)だけ並行移動して新たなモデル座標系を生成する場合,基準座標系から新しい座標系への同次変換行列Trans(x,y,z)は,
- 回転
基準となる座標系においてx軸を中心に右ねじまわりθ度だけ回転して新たなモデル座標系を生成する場合,基準座標系から新しい座標系への同次変換行列Rot(θ,x)は
同様にy軸,z軸に回転する場合は,
- 拡大,縮小
基準となる座標系においてx,y,z軸方向にそれぞれα,β,γ倍して新たなモデル座標系を生成する場合,基準座標系からモデル座標系への同次変換行列Scale(α,β,γ)は,
となります.このとき,回転成分に0を入れる(0倍する)と行列中の回転成分がおかしくなり,正しく変換されないことに注意しましょう.
同次変換行列を逐次掛け合わせることによって,より高度な座標変換をおこなうことができます.
例えば,太陽の周りを地球が公転していて,地球の周りを月が公転しているシーンを想像して下さい.太陽を描画するためには,太陽の位置に座標変換します.さらに地球は太陽を基準に公転するので,太陽の座標系からさらに座標変換します.月は地球を中心に回転しますので,同様に地球の座標系から逐次座標変換することになります.
このように平行移動,回転移動などの同次変換行列を組みあわせることによって,より複雑なモデルの記述を行うことができます.
ただし,逐次変換における変換の順序は非常に重要です.行列の乗算では交換法則が成立しないため,変換順序を間違えると座標系がおかしくなります.ここでは変換の順序が座標系にどう影響するか,具体的に述べます.
変換の順序でも混乱を招きやすいのが,回転と移動の混在です.例として次のような変換を考えます.
変換1は,座標系をx軸正方向に+aだけ平行移動します.また変換2は座標系をz軸まわりに
α度回転します.
変換1と変換2の順序を違えた場合,座標系はどのように変換されるか見てみましょう.
とします.T1は平行移動の後,回転をします.T_2は回転の後,平行移動します.具体的な計算をすると,
T1の回転成分とT2の回転成分は同じものとなっていますが,平行移動成分は異なっています.これを図に表わすと下図のようになります.
左側がT1による座標変換,右側がT2による変換です.平行移動と回転の順番が異なるだけで,このように座標系は大きく変わってきます.
実際のプログラムを記述する上では,座標系の変換順序が間違っていてもエラーにはならないため,誤りを発見しづらいハマりどころでもあります.十分注意しましょう.
同次変換行列の逐次変換をもちいた具体的な例として,本節では惑星の挙動を題材にして説明します.太陽と地球,月の位置姿勢を同次変換行列を用いて表わそうというものです.
それぞれの位置関係を上の図のようにとります.太陽の座標系をワールド座標系Wとして,地球座標系E,月座標系Mを決めます.
地球は太陽のまわりを半径Reの円軌道を描いて回転するものとします.また基準からの回転角度をθekとし,回転軸をzとします.
このとき,ワールド座標系Wから地球座標系Eへの変換は,
と表わせます.
月は地球のまわりを半径Rmの円軌道を描いて回転するものとします.基準からの回転角度をθmkとし,回転軸をzとします.
このとき,地球座標系Eから月座標系Eへの変換は,
したがって,ワールド座標系Wから月座標系Mへの変換は,
となります.
- 講義内容と同じ,太陽と地球,月の例で座標変換の演習をおこないます.最終的には図のような映像を作成します.
- 前回までのプログラムを参考にして,原点にワイヤーフレーム球(半径20)を描いてみましょう.
ワイヤーフレームの球にはglutWireSphere(double radius,int slices,int stacks)を使用します.radiusは球の半径,slicesは経線数,stacksには緯線数が入ります.経線数は36,緯線数は18程度で構いません
以下にサンプルを示しておきます.
void display(void)
{
glClear(GL_COLOR_BUFFER_BIT);
glLoadIdentity();
glutWireSphere(20.0, 36, 18);
glFlush();
glutSwapBuffers();
}
この例では描画の一番最初にglLoadIdentity()を使い,ワールド座標系(単位行列)を読み込んでいます.描画毎に座標系をリセットしていると考えて下さい.
- 平行移動の練習をしてみましょう.座標系をx軸方向(向かって右側)に60だけ平行移動して,半径10のワイヤーフレーム球を描いてみます.座標系を平行移動するコマンドはglTranslatef(float x, float y, float z)です.この場合は,平行移動してから地球を描きますので,
void display(void)
{
glClear(GL_COLOR_BUFFER_BIT);
glLoadIdentity();
glutWireSphere(20.0, 36, 18);/* 太陽 */
glTranslatef(60.0, 0.0, 0.0);/* x軸に60平行移動 */
glutWireSphere(10.0, 36, 18);/* 地球 */
glFlush();
glutSwapBuffers();
}
となります.
回転移動は前回にやった通りです.
平行移動,回転移動など座標変換する場合に注意があります.ひとたび座標変換すると,その効果が後ろにずっと続きます.上のプログラムの場合は,glLoadIdentity()で座標系をリセット(単位行列を代入)していますが,これがなくなると描画されるたびにglTranslatefが蓄積していきます.その結果,2つの球はx軸方向にすっ飛んでいきます(該当行をコメントアウトして試してみて下さい).
座標系を毎回リセットするほかに,ワールド座標系をずっと保管しておく方法もあります.次の例では行列のスタックを使用し,ワールド座標系を保管しています.
void display(void)
{
glClear(GL_COLOR_BUFFER_BIT);
glutWireSphere(20.0, 36, 18);/* 太陽 */
glPushMatrix();/* ワールド座標系の行列をスタックに入れる */
{
glTranslatef(60.0, 0.0, 0.0);/* x軸に60平行移動 */
glutWireSphere(10.0, 36, 18);/* 地球 */
}
glPopMatrix();/* ワールド座標系の行列を取り出す */
glFlush();
glutSwapBuffers();
}
このglPushMatrix()という関数は,座標変換行列を行列スタック*10にしまっておくという機能があり,glPopMatrix()では行列スタックから座標変換行列を取り出すという機能があります.
この関数の便利な使い方については後述します.ここでは座標変換行列をとりあえず保管しておくと考えておいて下さい.ただし,glPushMatrix()とglPopMatrix()は対で使用します*11.
-->

-
上で表示したものを太陽と地球として,地球を周回させてみましょう.
地球は太陽の周りを半径Re=60の円軌道を描くとします.基準からの回転角度をtheta_ekとして,一回の描画で1度回転するとします.
描画毎にパラメータを変更する方法はサンプルプログラムにもありましたが,下記を参考にして下さい.
void display(void)
{
static float theta_ek;
theta_ek+=1.0;
...
}
太陽(ワールド座標)から地球への座標変換は
で表せました.これを参考に作成してみて下さい.

(例)
void display(void)
{
static float theta_ek;
theta_ek+=1.0;
glClear(GL_COLOR_BUFFER_BIT);
glutWireSphere(20.0, 36, 18);/* 太陽 */
glPushMatrix();/* ワールド座標系の行列をスタックに入れる */
{
glRotatef(theta_ek, 0.0, 0.0, 1.0);/* z軸周りにtheta_ek度回転 */
glTranslatef(60.0, 0.0, 0.0);/* x軸に60平行移動 */
glutWireSphere(10.0, 36, 18);/* 地球 */
}
glPopMatrix();/* ワールド座標系の行列を取り出す */
glFlush();
glutSwapBuffers();
}
- 地球の周りに月を公転させてみましょう.月は半径5の球で,地球の周りを半径Rm=20の円軌道を描くとします.基準からの回転角度をtheta_mkとして,一回の描画で3度回転するとします.

(例)
void display(void)
{
static float theta_ek;
static float theta_mk;
theta_mk+=3.0;
theta_ek+=1.0;
glClear(GL_COLOR_BUFFER_BIT);
glutWireSphere(20.0, 36, 18);/* 太陽 */
glPushMatrix();/* ワールド座標系の行列をスタックに入れる */
{
glRotatef(theta_ek, 0.0, 0.0, 1.0);/* z軸周りにtheta_ek度回転 */
glTranslatef(60.0, 0.0, 0.0);/* x軸に60平行移動 */
glutWireSphere(10.0, 36, 18);/* 地球 */
glRotatef(theta_mk, 0.0, 0.0, 1.0);/* z軸周りにtheta_mk度回転 */
glTranslatef(20.0, 0.0, 0.0);/* x軸に20平行移動 */
glutWireSphere(5.0, 36, 18);/* 月 */
}
glPopMatrix();/* ワールド座標系の行列を取り出す */
glFlush();
glutSwapBuffers();
}
- 地球をz軸周りに自転させてみましょう.
地球を自転させるには,地球の座標系をさらに回転すればよいことになります.自転の角度パラメータをtheta_eとして,描画毎に5度減少するようにとってみると,
.....
static float theta_e;
theta_e-=5.0;
.....
glRotatef(theta_ek, 0.0, 0.0, 1.0);/* z軸周りにtheta_ek度回転 */
glTranslatef(60.0, 0.0, 0.0);/* x軸に60平行移動 */
glRotatef(theta_e, 0.0, 0.0, 1.0);/* z軸周りにtheta_e度回転 */
glutWireSphere(10.0, 36, 18);/* 地球 */
.....
とすれば良さそうですね.しかし,このまま後続の月を描画しようとすると,地球の自転につられて,月も一緒に回転してしまうことになります.このような場面でglPushMatrix()とglPopMatrix()は威力を発揮します.下の例を見て下さい.
glPushMatrix();/* ワールド座標系の行列をスタックに入れる */
{
glRotatef(theta_ek, 0.0, 0.0, 1.0);/* z軸周りにtheta_ek度回転 */
glTranslatef(60.0, 0.0, 0.0);/* x軸に60平行移動 */
/* ここまでで地球座標系 */
glPushMatrix();/* 地球座標系の行列をスタックに入れる */
{
glRotatef(theta_e, 0.0, 0.0, 1.0);/* z軸周りにtheta_e度回転 */
glutWireSphere(10.0, 36, 18);/* 地球 */
}
glPopMatrix();/* 地球座標系の行列を取り出す */
glRotatef(theta_mk, 0.0, 0.0, 1.0);/* z軸周りにtheta_mk度回転 */
glTranslatef(20.0, 0.0, 0.0);/* x軸に20平行移動 */
glutWireSphere(5.0, 36, 18);/* 月 */
}
glPopMatrix();/* ワールド座標系の行列を取り出す */
地球座標系の変換行列をglPushMatrix()で保管しておき,glPopMatrix()で再び取り出しています.
このように,後ろに影響を及ぼしたくない座標変換(この例では地球の自転)はglPushMatrix()とglPopMatrix()で囲むのがスマートです*12.
- 映像の見えを変更してみましょう
- 視点の位置や方向を変更するもっとも簡単な方法はgluLookAt関数を使うことです*13.
gluLookAt(float x,float y, float z,float cx,float cy,float cz,float ux,float uy,float uz)では視点位置を(x,y,z),注視点位置を(cx,cy,cz),頭上を表すベクトル(ux,uy,uz)で視点の位置と方向を定めます.
視点の位置を(x,y,z)=(0.0,30.0,10.0),注視点を(cx,cy,cz)=(0.0,0.0,0.0),頭上ベクトルを(ux,uy,uz)=(0.0,1.0,0.0)としてみましょう.gluLookAt関数もれっきとした座標変換ですので,前述したようにglLoadIdentity()を使うか,glPushMatrix()とglPopMatrix()を使ってワールド座標系を保護するようにして下さい.
- 遠くに行く物体ほど小さく見えるようにするには,正射影変換ではなく透視射影変換をおこないます.現在使用しているのは正射影変換であるglOrtho関数で,これを射影変換をするglFrustum関数に変更します.
この関数の引数はglFrustum(float left,float right,float bottom,float top,float near,float far)となっています.それぞれのパラメータは図に示している通りです.
図:透視射影変換の関数glFrustum(left, right, bottom, top, near, far) [1]より
例えば,視体積の大きさを
glFrustum(-50.0, 50.0, -50.0, 50.0, 30.0, 10000.0);
と指定し,視点の位置を
gluLookAt(0.0, 100.0, 20.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
とすると下記のような絵になります.パラメータを変えてみて,どのような効果になるのか考察してみましょう.
- 色と奥行き感をつけてみましょう.
- glColor3f関数を使用して,太陽を赤色,地球を青色,月を黄色にしてみましょう.
- 前後関係がおかしくなっていることに気づいたかと思います.描画が後ろのものが上書きされているような感じですね.これはデプスバッファが有効になっていないことに起因します.
デプスバッファを有効にするには,少しおまじないが必要です.
まずmain()関数の中にある,
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB);
を
glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGB);
に変更します.glutInitDisplayMode()関数ではウインドウの初期化時に表示モードを設定します.ここでは,デプスバッファ,ダブルバッファ*14,RGBモードが有効になっています.
次にinit()関数の中に次の一文を追加して下さい.
glEnable(GL_DEPTH_TEST);
これはデプスバッファを有効にするコマンドです.
最後にdisplay()関数にある
glClear(GL_COLOR_BUFFER_BIT);
を
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
に変更して下さい.このコマンドは描画の最初に呼ばれていますが,バッファをクリアするもので,ここではフレームバッファとデプスバッファをクリアしています.
ちなみにフレームバッファのクリア色を設定したい場合はglClearColor(float r,float g,float b,float alpha)で色を指定できます.alpha値は透明度を表すものですが,詳しくは後述します.
-
ワイヤーフレームでは少し味気ないので,ソリッドモデルに変更してみましょう.
glutWireSphereをglutSolidSphereに置き換えます.
これでも味気ないという向きには,init()関数に
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
glEnable(GL_COLOR_MATERIAL);
を加えてみて下さい.これは光源の設定と物体の質感の設定を有効にする関数です.光源と質感については別の回にやります.
- glutSolidSphere()の代わりにglutSolidTeapot(float size)を使ってみましょう.
- ここで勉強したOpenGLの関数は,
glLoadIdentity()
glTranslatef(float x,float y,float z)
glPushMatrix()
glPopMatrix()
glFrustrum(float left,float right,float bottom,float top,float near,float far)
gluLookAt(float eye_x,float eye_y,float eye_z,float cen_x,float cen_y,float cen_z,float vec_x,float vec_y,float vec_z)
glutInitDisplayMode(*****)
glEnable(GL_*****)
glClear(*****)
glClearColor(float r,float g,float b,float alpha)
glutSolidSphere(double radius,int slices,int stacks)
です.赤本を利用して,復習しておきましょう.
戻る