ここまでで2Dのゲームプログラミングを見てきました。ここまででもある程度高度な数学が現れています。例えば,w:ベクトルやw:三角比はその例でどちらも高等学校数学で扱われる題材で、割合高度な数学といえます。残念ながら3Dプログラミングでは更に高度な数学が必要となります。
3Dプログラミングでは4x4までのw:行列の演算が必要になります。2x2までの行列演算については高等学校数学Cを参照してください。また、任意の階数の行列演算については線型代数学を参照してください。具体的には、逆行列を求める演算(w:クラメールの公式、w:ガウスの消去法など)までを学習しておくとよいでしょう。
また、ゲームによっては初等的な力学の法則を扱う場合があります。残念ながら3次元空間での一般的な力学運動は高等学校物理の範囲を超えます。このような内容を習得するには,古典力学を読む必要があります。
いずれにしても、3Dプログラミングに必要な数学はw:理系の大学1, 2年程度で習う内容です。
w:3D (3 Dimension) プログラミングは3つの数値で表される座標を持つ物体を扱うプログラミング技法です。普通コンピュータで画面表示として用いられているw:ディスプレイは、厚みのある事柄を表示することはできません。これは、ディスプレイの表示方法が2つの座標(縦と横)で表される表示方法であることによります。一方,計算機上のデータとしては,3つの数値を組として扱うことは,2つの数値を組として扱うことと比べて格別に難しいことではありません。そのため、ある物体が存在する位置を3次元の座標で表し,その座標を操作することで物体の3次元中での移動などを扱うことができます。この時,最終的に操作の結果を表示するには2次元のディスプレイを扱う必要があるため,3次元の座標を2次元の座標に変換する方法が必要になります。
3次元の座標から2次元の座標を引き出す方法として,射影行列(線形代数などを参照)を利用する方法があげられます。射影行列
は,
![{\displaystyle \sum _{i}P_{i}=1}](https://wikimedia.org/api/rest_v1/media/math/render/svg/472b61ea0a6d5c037a7a855d2a8491233de23678)
![{\displaystyle P_{i}^{2}=P_{i}}](https://wikimedia.org/api/rest_v1/media/math/render/svg/01269383c84e231172d668967d8f8faa85ae5a31)
(和を取らない)を満たす行列のことです。ここでは、z軸方向を厚みの方向として取り,射影行列として,
![{\displaystyle P_{1}={\begin{pmatrix}1&0&0\\0&1&0\\0&0&0\end{pmatrix}}}](https://wikimedia.org/api/rest_v1/media/math/render/svg/58ed30254e6adbac1238dca8a10df66a6c3dbd0b)
![{\displaystyle P_{2}={\begin{pmatrix}0&0&0\\0&0&0\\0&0&1\end{pmatrix}}}](https://wikimedia.org/api/rest_v1/media/math/render/svg/40369ba500381c6fb42bafd328c596c828a2a9ff)
を取ります。この2つの行列は
を満たします。ここで、ある点
の3次元座標を
![{\displaystyle {\vec {x}}=(x,y,z)}](https://wikimedia.org/api/rest_v1/media/math/render/svg/2ecdc4cb0c225b2fdf59c9107da4a83004d7d007)
とした時,その点の2次元座標を
![{\displaystyle P_{1}{\vec {x}}=(x,y,0)}](https://wikimedia.org/api/rest_v1/media/math/render/svg/2a2907c88c1be2d97d48920d49c91298c70f3580)
のx, y成分で表します。これはあらゆる3次元座標にただ1つの2次元座標を与える射影で,3次元座標から2次元座標を取り出す1つの方法ということになります。
ここまでで3次元座標から2次元座標を取り出す方法を述べました。ここからは、実際にこの方法をプログラムする方法を述べます。上の方法を扱うには、行列の計算と、ベクトルを扱う方法が必要です。実際にはこれらの方法はいろいろなライブラリで扱われており(例えば,w:en:GSL(GNU Scientific Library)など),実際のプログラミングではそれらを用いるとよいでしょう。もちろん自力で実装することもできます。
上で述べた通り,行列やベクトルを扱う演算はプログラムで扱うこともできますが,実際にはそれはあまり行われません。それはそのような計算はw:CPUにとって手間がかかることが多く,全てをCPUを使って計算しようとすると多くの時間がかかるからです。このような計算を扱うためには,w:グラフィックボード、w:ビデオカードと呼ばれる機器を使うことが推奨されます。これらは、3次元の計算をするために特化した演算装置(CPUに対して,w:GPUと呼ばれることがある)を持っており,CPUを使って計算を行うよりも速く演算を行うことができます。ただし、そのような機器がない環境でも全ての計算をCPUを使って行うことで3Dの描画を行うことは可能です。
ここまでで、3Dの計算を扱うために専用の機器を使えることを述べました。ここからは実際にそのような機器を使う方法について述べます。また、そのような機器がなくとも,同じプログラムを使ってCPUに計算をさせる方法があることについても述べます。
実際に何らかの機器を用意した時,その機器に実際に計算をさせるための手法が必要になります。この手法は環境によって少し異なっています。各社が発売しているw:グラフィックボードのw:デバイスドライバには,それらの機器を利用者のプログラムから利用するための一連の関数が用意されています。関数群には通常2つがあり、これらはそれぞれw:DirectX、w:OpenGLと呼ばれます。Windowsからは両方の関数群が使えることが普通です。一方,他の環境(w:Mac OS X、w:Linuxなど)からはOpenGLしか使えません。これらの関数群は互いに互換性が無いので,実際に使用するときは注意が必要です。ここではOpenGLについて扱います。
ここで,グラフィックボードなどの機器が用意できないなどの理由で全ての計算をCPUに行わせる場合のことを述べます。この場合は基本的にグラフィックボードが提供する機能を1つ1つプログラムしていくことになります。実際には,このような場合にはw:en:Mesa 3Dなどのライブラリを使うのが1つの手段です。Mesaは、OpenGLが提供する機能を実際にプログラムしたもので、1つ1つプログラムする手間が省けます。Mesaはw:オープンソースライセンスで配布されているので自由に手に入れて使うとよいでしょう。
ここまでで3Dの計算を行う方法について述べました。ここでは実際にOpenGLのプログラムを扱います。OpenGLのチュートリアルはWeb上で公開されているので,紹介します。(OpenGL Redbook http://fly.srk.fer.hr/~unreal/theredbook/ ) また、OpenGLの詳細な規定は、http://www.opengl.org/documentation/specs/ で入手できます。
OpenGLを利用して計算を行うのですが、OpenGLが行うのはあくまで計算であり,画面への描画では無いことに注意が必要です。画面への描画はOpenGLでの計算結果に従って描画を扱うプログラムが行います。Linuxでは描画はw:X Window Systemが行い,Mac OS Xでは、w:quartzが行います。そのため、結果の描画を行うためには,描画を行うプログラムに,OpenGLを利用することを知らせる必要があります。
残念な事にこの作業を行う方法はウィンドウシステムによって異なっています。Xの場合にはglX関数を使い,Xに指示を出しますが、この方法は他のプラットフォームでは使えません。幸いにもこのような作業を様々なプラットフォームで使う方法として,w:GLUT、w:SDLなどを利用することができます。ここでは、SDLを利用する方法について述べます。
GLUTは本来GLの利用を簡単にするために作られました。一方,SDLは本来ゲームプログラミングを行うことを目的として作られたライブラリです。しかし、いずれにしろ3Dプログラミングを行うためにはOpenGLの利用は必須なので,SDLもGLUTに近い機能を持っています。どちらもWindows, Mac OS X, Linux上でOpenGLの動作を始めることができます。
SDLでOpenGLを利用するときには
#include <SDL.h>
#include <GL/gl.h>
int main()
{
if (SDL_SetVideoMode(200, 200, 0, SDL_OPENGL) == NULL)
return 10;
/* OpenGLの処理 */
SDL_GL_SwapBuffers();
return 0;
}
とします。ここではこのプログラムについて順に説明していきます。まず、OpenGLを使わなくとも、SDLを利用する場合でも、1, 3, 4, 5, 6, 9, 10行は必要となります。ただし,4行目は、
SDL_Surface *surf;
if (surf = SDL_SetVideoMode(200, 200, 0, 0))
などと書き換える必要があります。
- 1行目は,SDLを利用する際に必要となるヘッダファイルです。これは常に必要になります。
- 2行目はOpenGLの関数を利用する時に必要となるヘッダです。この部分は例え,GLUTを利用するとしても,変わりはありません。それは、SDLであるにしろ,GLUTであるにしろどちらもウィンドウシステムからOpenGLの領域を受け取ることに変わりはないからです。そのため、利用するヘッダも、2つのライブラリで共通のものを利用します。
- 3,4, 9, 10行目はほとんど全てのCプログラムで必要になります。
- 5行目でOpenGLの領域をウィンドウシステムから受け取っています。SDL_SetVideoModeは本来のSDLでは書き換えられたプログラムの5行目のように描画を行うSDL_Surfaceという構造体を返します。しかし、4つめの引数にSDL_OPENGLのw:フラグを受け取った時には,OpenGLの領域を初期化するように動作が変化します。仮にこの動作が失敗した場合には,この関数はNULLを返すので,その場合にはここでプログラムを終了するのが普通です。
- 7行目以降でOpenGLの関数を書きます。ここについては後で述べます。
- 8行目ではSDL_GL_SwapBuffersという関数を使っています。この関数はOpenGLの関数で描画した図形を、実際にウィンドウに描画する関数です。この方法はw:ダブルバッファを簡単に実現してくれます。GLで描画された内容が実際に表示されるのは,この関数以降です。また、実際に描画を行ってもこのままではすぐにプログラムが終了してしまうので,描画結果を見るためには,whileループかsleepなどの関数を利用して,プログラムの実行を続ける必要があります。実際にはSDLはSDL_Delayというプログラムの実行を一時的に遅らせる関数を持っているので,この関数を使うのもよいでしょう。
ここからは、実際にOpenGLの関数を書いていきます。ここで書かれるプログラムは上のプログラムの7行目以降に書かれるものです。
glBegin(GL_LINE_LOOP);
glVertex3d(0,0,0);
glVertex3d(0,1,0);
glVertex3d(1,0,0);
glEnd();
このプログラム(tri.cとする)をコンパイルするには,いくつかのコンパイラオプションが必要です。コンパイラがw:GCCなら、
$gcc -Wall tri.c -L libGLのある場所 -I GL/gl.hのある場所 -lGL $(sdl-config --cflags --libs)
となります。実際にGLの関数が置かれている場所は環境によります。X+MesaではXが置かれているディレクトリにあることが普通ですが,グラフィックボードのドライバを利用するなら,w:メーカーの指定したディレクトリを使う必要があります。また、上の例では必要ないですが,-lGLUをつける場合もあります。これはglu.hで定義された関数を利用する場合に使います。glu.hの関数は実際にはgl.hの関数をいくつか組み合わせた関数ですが,よく用いられる組み合わせなので、gl.hの関数とともに配布されます。
このプログラムを実行すると,黒い背景の中に白い3角形が描かれるはずです。
![](https://upload.wikimedia.org/wikibooks/ja/d/d8/Gl_tri.png)
上のプログラムの結果を説明するにはいくつかの事柄を説明する必要があります。まず、上のプログラム内で,
glBegin(), glEnd()
という関数が使われています。この関数はその中で指定された事柄(主に頂点に関する事柄)を記録させます。記録できる事柄は頂点の位置や色などがあります。頂点の位置を指定するには
glVertex3f()
を使うことができます。ここでこの関数名はOpenGLに特徴的な事柄をいくつか含んでいます。最後の3fがそれでこれはそれぞれこの関数が"3つ"の引数を取ることと、その型が"GLfloat"であることです。ここでGLfloatはほとんどの場合float型のtypedefになっています。これ以降にもこの種の関数名は何度か出てきます。glVertex3fは、3つの引数によってそれぞれ3次元の座標を指定します。
さて、ここまでで上のGLプログラムの1-5行が、3つの座標を指定したことがわかります。実際にそれらの座標をどう使うかについてはglBegin()の引数が使われます。ここではGL_LINE_LOOPを使ったので,指定された座標は指定された順に直線でつながれます。
ここまでで、上のプログラム中で座標 (0,0,0),(0,1,0),(0,0,1) が指定され,これらが直線でつながれることがわかりました。これは3次元中で3角形を描くことがわかります。次の問題は,この三角形がどのようにディスプレイに投影されるかです。
OpenGLは3次元中に指定された座標の2次元中での位置を定めるために2つの行列を利用します。ここで、OpenGLでは行列を"扱う"場所は1つしか用意しなくてもいいことが定められています。もちろん、値を保存しておく場所は複数あるのですが,これらの値を変更するのに使う領域は1つです。そのため、これらの行列を使い分けるには,これらを所定の場所に呼び出す必要があります。ここで行列を指定するには,glMatrixMode()関数を使います。この関数は1つの引数を取り、その値で行列を指定します。2次元中での位置を指定するのに使う行列はそれぞれGL_MODELVIEW, GL_PEROJECTIONで指定されます。これらの行列の初期値は4x4のw:単位行列です。また、実際に使われるのはこれらの積であるので,値を変更するのは片方の行列だけでもあらゆる変換を指定することは可能です。
ここで2つの行列のそれぞれについてその性質を述べます。GL_MODELVIEW行列はそれぞれの頂点の座標を変更するのに利用します。実際には大抵の場合、この行列は
glRotatef(), glScalef(), glTranslatef()
の3つの関数から利用されます。 これらの関数は順に,座標を"回転", "拡大縮小", "平行移動"します。これらの関数はそれぞれ対応する変換を行う行列を生成し、選択されている行列を変換します。ここではそれらの行列の詳細について解説します。対応する行列についてはそれぞれのmanを参照してください。(OpenGLのspecification http://www.opengl.org/documentation/specs/man_pages/hardcopy/GL/html/ )
translateの生成する行列は、
![{\displaystyle {\begin{pmatrix}1&0&0&x\\0&1&0&y\\0&0&1&z\\0&0&0&1\end{pmatrix}}}](https://wikimedia.org/api/rest_v1/media/math/render/svg/502a28bde7d327148886e1342405a4272747115a)
で与えられます。この行列は、元の頂点の3次元座標が
で表されるときの同次座標
を、
に変換しますが、これはまさに平行移動によって引き起こされる座標変換に対応しています。
scaleの生成する行列は、
![{\displaystyle {\begin{pmatrix}x&0&0&0\\0&y&0&0\\0&0&z&0\\0&0&0&1\end{pmatrix}}}](https://wikimedia.org/api/rest_v1/media/math/render/svg/1123f35f69ec7c73561365ddeee2b105c9292e01)
です。この変換は3次元座標
を、
に変換します。これはx,y,zの値によってそれぞれの座標を拡大縮小する変換に対応します。
最後に、rotateで得られる行列を紹介します。
![{\displaystyle {\begin{pmatrix}n_{x}^{2}(1-c)+c&n_{x}n_{y}(1-c)-n_{z}s&n_{x}n_{z}(1-c)+n_{y}s&0\\n_{x}n_{y}(1-c)+n_{z}s&n_{y}^{2}(1-c)+c&n_{y}n_{z}(1-c)-n_{x}s&0\\n_{x}n_{z}(1-c)-n_{y}s&n_{y}n_{z}(1-c)+n_{x}s&n_{z}^{2}(1-c)+c&0\\0&0&0&1\end{pmatrix}}}](https://wikimedia.org/api/rest_v1/media/math/render/svg/e98585b2bfdc4e463eceb66299bee723cf42ed64)
この変換の導出は少し厄介です。まず、原点から始まり、回転の方向を向いている単位ベクトルを
とし、ある頂点の座標を表すベクトルを
とします。このとき、この回転で変化するのは、
のうち、
に平行でない成分のみです。
に平行な成分は、
![{\displaystyle {\vec {a}}_{\parallel }=({\vec {a}}\cdot {\vec {n}}){\vec {n}}}](https://wikimedia.org/api/rest_v1/media/math/render/svg/27dafbcd8b03b8149a0c1988b88492a1626c865c)
で表されるので、実際に回転によって変化する成分は、
から上の分を取り除いたベクトルです。このベクトルを
と呼びます。このベクトルは回転によって変化しますが、これを表すには、
と
の両方に直交するベクトルが必要です。このベクトルを、
![{\displaystyle {\vec {b}}={\vec {n}}\times {\vec {a}}_{\perp }}](https://wikimedia.org/api/rest_v1/media/math/render/svg/45214142ca058e13aa896d83f9b5483393b60075)
と取ります。このとき、回転する角度を
とすると、後のベクトルは、
と書けます。
結局得られるベクトルは、
![{\displaystyle {\vec {a}}_{\parallel }+{\vec {r}}}](https://wikimedia.org/api/rest_v1/media/math/render/svg/abecec17ff9e5eaac0f961a23a0d77eec557dbb6)
となります。ここからは、この結果が上の式と一致することを示すため、この式を整理していきます。まず、
について、3x3行列
![{\displaystyle N={\vec {n}}{}^{t}{\vec {n}}={\begin{pmatrix}n_{x}^{2}&n_{x}n_{y}&n_{z}n_{x}\\n_{x}n_{y}&n_{y}^{2}&n_{y}n_{z}\\n_{z}n_{x}&n_{y}n_{z}&n_{z}^{2}\end{pmatrix}}}](https://wikimedia.org/api/rest_v1/media/math/render/svg/ffd9e7d3bc231ea06cced95aa64f8770a8d9dc77)
を定義すると、
が成り立ちます。また、
について、3x3行列
![{\displaystyle R={\begin{pmatrix}0&-n_{z}&n_{y}\\n_{z}&0&-n_{x}\\-n_{y}&n_{x}&0\end{pmatrix}}}](https://wikimedia.org/api/rest_v1/media/math/render/svg/97095e8e54b380a9e64638da441ce868df0d722e)
を定義すると、
が成り立ちます。このことから結果の式について、
![{\displaystyle {\vec {a}}_{\parallel }+{\vec {r}}}](https://wikimedia.org/api/rest_v1/media/math/render/svg/abecec17ff9e5eaac0f961a23a0d77eec557dbb6)
![{\displaystyle =N{\vec {a}}+\cos \theta ({\vec {a}}-N{\vec {a}})+\sin \theta R{\vec {a}}}](https://wikimedia.org/api/rest_v1/media/math/render/svg/ae39b033879ad90bfe1e9d27b063f97617b81bb7)
が得られますが、これを整理すると求めていた行列x
が現れます。
次に、GL_PROJECTION行列について述べます。OpenGLでは、頂点の座標についてこれらの行列をかけた後 (-1, -1, -1), (1,1,1) で指定される立方体内にある図形を描きます。このため、例えば (0,0,10) にある図形は一般には描かれません。しかし、図形を3次元中に描くときの事情で、これらの位置を自由に指定できないと不便です。GL_PROJECTION行列は"描画される範囲を広げる"目的で利用されます。また、この行列は、"遠くにあるものほど小さく見える"という状況を扱うためにも利用されます。
まず、"描画される範囲を広げる"場合について行列の指定の仕方を説明します。実際には範囲を広げているのでは無く,"(-1,-1,-1)(1,1,1) 内に入るように、遠くにあるものを近くに持ってきている"のが正しいのですが、どちらにしろ結果は同じです。これを指定するにはglOrtho()関数を利用します。glOrtho関数は6つの引数を取り,それぞれ描画する立方体(一般には直方体)を指定する座標を取ります。引数は順に
glOrtho(left, right, bottom, top, near, far)
と呼ばれ,描画される直方体は(left, bottom, near)(right, top, far)で指定されます。このとき、glOrthoを利用しないときは
glOrtho(-1, 1, -1, 1, -1, 1)
が指定されたときと同じ結果になることがわかりますが、実際にはこの指定は4x4の単位行列を返すので指定を省略したときと結果は同じになります。対応する行列は
となります
次に、"遠くにあるものほど小さく見える"の場合について説明します。これを指定するためには,いくつかの変数が必要になります。まず、"物体を観察するもの"が (0,0,0) にあるとします。次に,遠くの物体が縮小される割合を指定します。これは、投影された後のx座標とy座標に対応して2つのパラメータが必要です。更に,見える方向がz方向で無い場合を扱うために2つのパラメータが必要となります。最後に,上で指定した角度の方向に実際に描画するz座標の範囲を指定します。これらを全て指定すると、描画される領域は角錐 (w:en:frustum) になります。
実際にこの領域を指定するにはglFrustum関数を利用します。glFrustum関数は6つの引数を取りますが,それぞれの引数は
glFrustum(left, right, top, bottom, near, far)
で与えられる名前をもっており、縮小される割合を定める四角形がl, r, t, b、四角形の位置がn、描画される範囲がfで定められます。glFrustumで与えられる行列は
です。