ゲームプログラミング/3Dグラフィック

出典: フリー教科書『ウィキブックス(Wikibooks)』
ナビゲーションに移動 検索に移動

3Dグラフィック[編集]

計算の原理[編集]

3D-コンピュータグラフィックの計算の方式は、投影面(スクリーン)の形状(おおまかに2種類)と、平行投影か透視投影かの2種類の違いによって、2×2=4種類ていどに分かれる。

投影面

  1. 平面投影:
  2. 円柱面投影などの非平面投影:

投影の方式

透視投影
  1. 透視投影
  2. 平行投影


一般的には、平面投影と透視投影の組み合わせが単純なので、よく使われる。

大学の3D-CG学などで単に「投視投影」と言った場合、この平面投影での透視投影の方式のことを言う場合が多い。

投影面[編集]

投影面の種類は主に2~3種類、

  1. 平面投影: 投影面を平面として考えて、観測者と被写体をむすぶ線分と、投影面との交点を求めて出力画像として表示する方式。
  2. 円柱面投影: 投影面を円柱面で考える方式。

くらいである。

このほか、球面投影や、楕円などで考える楕円柱投影や楕円球投影なども理論的にはありうるが、非実用的なので(球面投影に対応した安価な映像デバイスが無い。楕円だと設計のための計算が複雑な割りに、得られる画像も知見もあまり平面投影や円柱投影などと変わらないので)、楕円については考察や説明を除外する。

さて、紹介した平面・円柱の2種の投影面のうち、いちばん単純かつ普及していて実用的な投影面は、平面投影である。

なので、初学者はまず平面投影を中心的に学ぶのが良いだろう。なお、後述する単元での隠面処理のためにカメラの向きの角度計算が必要になるので、円柱投影や球面投影に近い考え方も必要になるので、べつに円柱投影などを学んでしまっても損は無い。

ただし、円柱投影には欠点があり、上下方向と左右方向で透視の縮尺が違うために、視界内で回転する回転体をうまく表現しづらいという致命的な欠点がある。たとえば、回転する音楽レコード盤を見下ろしているとき、円柱投影では、レコードが(工夫しないと)楕円にゆがんでしまう。

球面投影なら、レコードを見ても ゆがんだりしないが、しかし球面投影に適した映写デバイスが存在しない。

なので、結局、ふつうは平面投影で計算することになる。


さて、じつは、平面投影と円柱投影の2つの方式を比べた場合、得られる画像が微妙に異なる。カメラが向きを変えたときに、どのくらい視界内で被写体の位置が変化するかが、それぞれの投影面ごとに変化する。

そして、特に観測者の真横に近い位置になると、特に被写体の動きの変化量の違いが大きくなる。


平面投影の方式の場合、投影面が観測者の視界の向きで、少し前方にあるので、そもそも真横に近い場所にある物体は正投影が無理である。

だが、通常の人間の視界では、真横にある物体には意識が向かないので、なので平面投影でも問題ない。

なお、平面投影の欠点をこう聞くと、てっきり「円柱投影や球面投影のほうが視界にできる領域が広そう」(←誤解)に思われるかもしれないが、しかし実際には数値計算の誤差の理由により、つまり、円柱投影では観測者の真横にある物体は、投影のための数値計算(逆三角関数などの計算)のさいのケタ落ちが大きくなるので、円柱投影でも真横の物体の画像の表示は、不正確な表示になる。

なので、平面投影にしろ円柱投影にしろ、結局どちらとも、観測者の真横にある物体のCG画像表示は、そのままでは不正確な表示になってしまう。


円柱面は、幾何学的には「可展面」(かてんめん)である。トイレットペーパーや巻物を考えれば分かるように、円柱はハサミを入れるなどして展開すれば平面に展開することができる。

しかし球面は可展面ではない。なので、もしも球面投影の方式で3D-CGの投影を考えた場合、地図のメルカトル図法やモルワイデ図法などと同様の問題が起きるので、そういった対策が必要になる。


さて、平面投影は、数学的には単に直線と平面の交点を求めるだけの初等的な方式である。また、2次元で考えた場合の計算式(高校1年あたりで習うような、2点を結ぶ直線と、別の直線との交点を求める計算式)を、比較的に容易に3次元に拡張しやすい。

いっぽうで、円柱投影の場合、長所としては、わざわざ交点を求めなくても、(高校で習うような)三角関数の余弦定理などを使える。だが、球面投影や円柱投影の場合の短所として、その「角度」を算出するのが数学的にやや難しいことと、2次元で考えた結果を3次元に対応するのが難しい。


円柱投影の場合、必要な情報は角度だけなので、円柱の半径または球の半径については無限小であると考えてよい(プログラム上では、半径を書かなくても視界画像を算出できるので)。

すべての投影面で共通する性質[編集]

さて、平面投影・円柱投影のどれでも、透視投影ならば共通する性質がある。

それは、もし視界内に複数の点があり、色の異なる点だとして、視界内で観測者の近くにある点によって、観測者の遠くにある点が隠れるとき、それぞれの点の前後関係は、どの投影面でも同じであるという事である。


まず、観測者のカメラ位置を表す点Aと、1つの被写体を代表する点Bを考える。そして、線分ABを延長した無限長の直線Lを考えよう。

そして、直線L上で、仮に点Cが、点Bよりもカメラから遠い位置に(点Cが)あるとしよう。


このような場合で、もし平面投影の場合は、投影面の平面よりも奥にある被写体だけを写すことを考えている。つまり、点A,点Bなは、ともに投影面よりもカメラから遠い位置にある。なので、空間内での2点A,Bの前後関係には、投影面は何の変化も与えない。


このことから原理的には、空間内での前後関係の判定には、単にカメラから放射状に放出される無数の直線を考えて、それぞれの各直線ごとに、その直線上にある複数の点ごとに、カメラからの距離を算出し、カメラから最も近い距離にある点の色だけを視界に表現すれば、前後関係を原理的には簡単に算出できる(Zバッファ法の実装はおそらく、このようになるだろう)。

Zバッファ法は、アルゴリズム的には単純であるが、計算量について難点があり、カメラから放射状に放出する視界計算用の直線(きっと 何百本~何千本・何万本もある)ごとに、それぞれ前後関係を算出しないといけないので、コンピュータの処理速度の高さなどが必要である。

ただし点ごとにZバッファ法で計算すると計算量が膨大になってしまうので、改良法として、面ごとにZバッファ法を計算するという方法もある。

この面ごとのZバッファ法の場合、あくまで近似的なもので、本来なら「面」でなく「点」でないと数学的には正しく前後関係を判定できないので、前後判定される面の大きさとしては、事前になるべく小さな面として パソコン内では内部処理しておく必要がある。


また、もし一箇所に複数の面があると、うまく前後判定ができない場合の生じることもある。だが普通のゲームでは、そういう一箇所に複数の面が集まる自体はまず無いし、仮に間違った前後判定になっても、事前に面を小さく分割してあれば、間違いの影響も小さくなる。


ただし、面を小さく分割すれば分割するほど、計算量の負担は増えていく。


現在では、GPUなどのグラフィックボードにより、このような面ごとのZバッファ法ていどの計算なら、ある程度の新しいパソコンならば、瞬時で可能であろう。

なお、カメラから被写体のあいだの距離のことを「z値」という。Zバッファ法のZとは、そのZ値のことだろう。


Zバッファ法のほかにも「Zソート法」というのがある。「Zソート法」と「Zバッファ法」の違いは、Zバッファ法が各点ごとに計算している一方、「Zソート法」は(点ごと ではなく)大きさをもつ被写体ごとに、奥行きをあらわす値としてZ値を与えておく方式である。

Zソート法は 点ごと ではないので、コンピュータの計算の負担は少ないが、アルゴリズムが複雑化しやすい欠点がある。また、くぼんだ部分のある被写体など、複雑な形状の被写体では、不正確な前後判定になる。

(Zソート法だろうZバッファ法だろうがなんだろうが、数値の大きさにもとづいた並び替えが必要になるが、このような処理は『ゲームプログラミング/RPG#素早さ順行動のアルゴリズム』などにもあるので、参考になるかと。)


回転などの計算[編集]

カメラの向きを変えたり、被写体をどこかを中心軸にして回転したあとの座標を計算したい場合は、

単に、高校数学~大学1年ていどの行列の理論の、回転行列の公式をつかえばいい。


なお、四元数でも算出できるが、それは単位、3次元行列または4次元行列の計算を、四元数で置きかえただけの仕組みである。


なお、マイクロソフト社Windowsの3D-CGライブラリのDirectXを使うと4×4行列が出てくるが、これは単に、平行移動も回転行列と一緒に行列であつかえるようにするために、次元を1つ増やしてベクトルを4次元にして、対応する行列を4×4行列にしただけのものである。

数学の教育体系的には、大学1年くらいで習う「行列の同次形(どうじけい)」という理論の応用にすぎない。


行列の同次形とは[編集]

回転行列にかぎらず、2×2行列はそれと同内容で一対一に対応する3×3の行列がある。

証明は下記のとおり。(なんと、ネットを検索しても、証明がなかなか、みつからない! 数学書には普通に書いてあるんだが・・・。やはり勉強をする手段は、書物を原則とするにかぎる。)


まず仮に)、2次元の入力ベクトル V を

とする。(背理法でないので、特に疑わなくていい。)

そして変換行列 A を

とする。(※ 回転行列でなくてもいいのだが、回転行列を念頭においておくと、次の文の応用がイメージしやすい。)


すると、行列によるベクトルの変換操作は、

は、こう書ける。(ここまで高校3年のいわゆる「数学C」の範囲。年度により、行列が高校課程から抜けたりする時代もあるが。)

そして、平行移動 をこれに加えるには、単に、平行移動の移動方向に等しいベクトルPを用意して、

と、ベクトルPを足し算すればいい。


では、上記をもとに、これから行列の「同次形」の概念を説明する。

まず、行列Aとよく似た3次元行列B

を用意する。この行列Bが、行列Aの「同次形」である。

そして、入力ベクトルVによく似た、つぎの3次元ベクトルFを用意しよう。

積AVと同様に、積BFを(数学Cで習うような行列の積の)定義にそって求めてみると、

この積BFのベクトルの第1次元と第2次元を見てみると、すでに計算ずみのベクトル AV+P の第1次元と第2次元と同じである。

このように、2次元のベクトルに対する行列変換と平行移動(ベクトル同士の加減算)の合成は、3次元のベクトルに対する行列変換だけに置き換えることができる。

この、上述の行列Bのような形の行列が、「行列の同次形」というものである。ひとことでまとめて、上述のBの形の行列のことを「同次行列」ともいう。次元が変わるのに「同次」というのは奇妙(きみょう)だが、この理由は単に、もとになった英語 Homogeneous を数学者が和訳するときに「同次」と翻訳してしまっただけのことである。 Homogeneous とは「同質」・「同類」のような意味である。

なお、同次行列のことを「斉次」(せいじ)行列ともいう。

3次元ベクトルおよびその変換行列も、同様の計算法により、4次元の同次ベクトルと同次行列とに置き換えることができる。

平面投影の透視投影[編集]

原理と公式[編集]

一般に平面投影での透視投影では、計算の単純化のため、カメラの向きはZ軸の方向に固定する。

また、カメラの位置は原点に固定する。


カメラを自転させた場合の映像を描写したい場合は、代わりに被写体すべてを反対方向に回転(交点)させることで対応する。


このような仮定は、広く普及している。


この場合、透視投影は、中学レベルの簡単な相似で計算できるので、じつは簡単な比例式で表される。

あるいは、式をz関連の変数どうしてまとめて書き換えて、

とも書ける。


ただし、投影面の位置を zs とした。(添字sは「スクリーン」のつもり)

また、 z > zs に位置する被写体だけを描画するものとしている。

なお、この仮定の場合、z軸そのものの座標は x=0、y=0 である。

欠点と対策[編集]

このように簡易で表現力の高い平面スクリーン透視投影にも、いくつか欠点がある。

欠点1

欠点のひとつは、観測者のナナメ前方にある被写体が、実際よりも大きく見えてしまうことである。


極端な例をあげると、観測者の真横90度の場所に2メートル離れたところにある被写体は、さきほど紹介した公式に当てはめると無限大に拡大して見える。

このような現象の起きる理由は、遠くの物体の縮小率は、変数zだけによって 倍に縮小するからである。


なので、どんなに真横で何十メートルも離れた位置にいても、真横だとz=0なので倍率は 1/0 = ±∞ となり、被写体は無限大に拡大して見えることになる。


この対策として、真横に近い物体は最初から描画を除外すればいい。

つまり、視界に入る角度を設定しておき、それはたとえば 右60度~左60度 までを視界と定めておき、そこから外れた物体は描画を除外すればいい。


このような処理を可能にさせるために必要な計算として、現在の視界内での被写体の角度 位置を計算する必要がある。その角度の計算のための単純な方法として、単に三角関数 tan タンジェントを使えばいい。

たとえば、 視界内でのx軸方向の角度として 、およびy軸方向の角度として を計算して、両方のタンジェントとも両方とも一定値の範囲内なら、描画すればいい。

ただし、この方法では、zは一定値以上(たとえばカメラとスクリーンとの距離)でなければならない。


また、近似的な方法だが、三角関数を計算するのではなく、単に、xが一定値の範囲内、yが一定値の範囲内とする方法もある。このx,yを一定値の範囲内とする方法だと遠方にある被写体を描画できないが、そもそもゲームでは遠方にあるキャラクターなどの描画は不要だろう。

ただし、遠方にある山や川などの景色などは、別の方法で描画する必要があろう。


欠点2

平面スクリーンの透視投影では、欠点として、投影スクリーンよりも手前にきた被写体は、根本的に描画できない。

かといって、観測者からスクリーンまでの距離を小さくしすぎると、ゲーム的に不便になる場合がある。


妥協案としては色々あるが、ゲームとして面白ければいいので、一例として、もし被写体がスクリーンよりも手前に来たら、それらの被写体の描画だけ平行投影に切り替えるなどの対策があるだろう。

つまり、ゲーム画面は、(スクリーン幕よりも奥の被写体の)透視投影と(スクリーン手前の被写体の)平行投影との合成になる。


被写体の一部がカメラの裏側に回り込んだら[編集]

さて、被写体の一部がカメラの裏側に回り込んだ場合、上記のように単純に、その点の位置だけで描画の有無をきめる場合だと、

たとえば大きな三角形を描画したい場合など、もし、その三角形の3つの頂点のうちの1点だけですらカメラ裏に回り込んだら、もはやその三角形が一部すらも描画できなくなってしまうが(三角形の描画は3点が必要なので)、これは不合理である(一部の頂点が隠れただけなのに、三角形全体が消えてしまうので)。


このような問題への対策は特に決まってはないが一例として、たとえば図形の各頂点などの座標の位置とは別に、さらに代表位置の座標を用意すればよく、つまり代表の位置のx,y,zの座標の変数をそれぞれ用意するのが簡単であろう。

代表の位置は、べつに重心でも垂心でも三点の平均値でも何でもよく、好きなように決めればいい。(べつに面積計算をするわけではないので、重心でなくてもよいだろう)

平面への平行投影[編集]

カメラはz軸を向いているとする。

点(x,y,z)を平面スクリーン上に平行投影する場合、単に、zを無視して x,y の値がそのままスクリーンに投影される。

ただし、実際には、奥にある被写体は手前の被写体で隠れるので、zソート法などで隠面処理を考えた描画をする必要はある。


原理を実行したい場合に必要なプログラム環境[編集]

もし、(冒頭の章で述べたような)余弦定理による視界の計算をC言語で実行したい場合、

まず、三角関数(逆三角関数も使う)の計算と、平方根の計算が、どうしても必要になる。

C言語で三角関数と平方根を実行させたい場合、標準入出力のヘッダには、これらの数学関数が用意されてないので、

スースコード冒頭にインクルード文

#include <math.h>

で、まず数学関数のヘッダをインクルードする必要がある。


これさえ分かれば、あとは、画面出力の機能のあるプログラム言語を使えば、原理的には3D-CGのプログラムが作れる。

(ただし、国際規格になっている標準C言語 そのものには、画像表示の機能は無い。なので、もしWindowsの場合、たとえば Visual C++ や Visual C# などを使うことになる。本ページではVisual C++ および Visual C# 固有の話題については省略する。)


3D-CGのプログラムは、かならずしもC言語系の言語である必要は無い。

最低限度に必要なのは、画像表示の機能と、逆三角関数と平方根ていどの数学計算の組み込み関数さえ出来ればよい。

ただし、実際には、キーボードなどからの入力の機能なども必要なので、自分の使用したいプログラム言語で、それらのプログラムの方法も調べる必要がある。


なので、原理的にはスクリプト言語でもよく、たとえば JavaScript や python などでも、かまいません。(しかし3D-CGプログラムの場合、商品となるレベルにするには、処理速度の都合で、C言語系でプログラムするのが普通だが。)


もっとも、多くの実用では、自分でC言語でCGプログラムを作ることはしない場合が多く、既成の DirectX や OpenGL などのプログラムを利用するのが一般的です。

それでも、どうしても自作で3D-CG表示プログラムを作ってみたい場合、まず最低限の知識として、上記の逆三角関数、インクルードの方法、キーボード入力機能をもつプログラミングの作成方法、使用するプログラム言語による画面表示の方法、などの知識があることが前提になる。

これらの知識を持たない場合、先にそれらの知識を習得したほうが良い。


なお、C言語にベクトルや行列の機能は無い。なので、もしC言語系のプログラム言語で、ベクトルや行列などに相当する計算をコンピューターにさせる場合は、まず変数をいくつも宣言して、必要なベクトル計算や行列計算のプログラムを作ることになる。

3次元ベクトルを宣言するには、単に、異なる変数を3個、宣言すればいい。

回転行列などの行列は、ここでは単に、座標ベクトルなどのベクトルの変換作業にすぎないから、行列に相当するプログラムを書けばいい。なので、わざわざ行列をつくる必要も無い。(同様に四元数も、わざわざコード中で宣言して作成する必要は無い。)



プログラム例[編集]

円柱面投影の透視投影[編集]

で扱う。

中間まとめ[編集]

まとめると、平面投影にしろ円柱投影にしろ、

ゲーム映像の場合、原則を透視投影にしても結局、スクリーンよりも手前に来た被写体は、(投資投影でなく)平行投影など別の投影アルゴリズムで描写することになります。

また、スクリーンの奥側でも、真横の方向に近い位置にある被写体は、(アルゴリズムにもよりますが)透視投影ではケタ落ち等が起こりやすいので、平行投影などに切り替える必要があります。


このため、角度または内積を基準として、透視投影の描画の条件を満たした角度位置または内積となる被写体の場合にだけ、被写体を透視投影として描画することになるでしょう。

隠面処理[編集]

「隠面処理」とは、隠れた面を表示しない方法のこと。

手前の物体で隠れる部分は、当然、画面に表示されないように工夫する必要がある。

たとえば、ある面Aによって、ある面Bが隠される場合、画面にBは表示させないようにする必要がある。

このための手法はいろいろあるが、共通する原理は、カメラからの向きごとにあるそれぞれの向きごとに、その向きにある複数の被写体(面Aと面B)ごとにカメラから被写体の面の距離(z値)を計算しておき、カメラから遠いほうの面が描かれないようにすればいいのである。


単純なアルゴリズムでこれを行うなら、被写体のそれぞれの面に与える情報としては、Z値のほかにも、カメラからの角度も、面の情報として残しておく必要がある。

カメラからの角度が同じ面どうしで、カメラからの距離を比べるわけである。


カメラから近いほうの物体や面を先に描く方法を、Zバッファ法という。(なお、ほかの方式としては、面ではなく点ごとにカメラから近い物体を書く方法もあるが、しかし計算量が膨大になるために処理速度が悪化する。なので、ゲームのCG手法としては、点ごとのZバッファ法は、あまり普及していない。ただし、(処理速度の必要な)ゲームではなく(映像の緻密さの必要な)商業アニメのプリレンダリングなどの場合ならば、目的・用途に応じて面ではなく点でZバッファするのも良いだろう。目的に応じて手法を使い分ける必要がある。)


被写体が不透明なら、カメラから遠い物体を省略できるので、処理を高速化しやすく、そのため、ゲームにもよく用いられている。

しかし、被写体が透明/半透明な物体の場合に、アルゴリズムが複雑化するという欠点がある。


いっぽう、カメラから遠くから先に描画する場合をZソート法という。被写体に透明の物体を含む場合は、Zソートで描画せざるを得ないだろう。

そもそも論[編集]

そもそも論として、かならずしも3D計算を行わなくても、ゲームとして立体的な描画をできる場合があります。

また、既存の商用3Dソフトウェアやフリーソフトなどを使うことにより、自分でプログラムする必要のない場合もあります。

また、そもそも日本のゲーム産業では、3D描画はあまり儲かってなく、儲かってるのはアニメ絵の2次元絵イラストのソーシャルゲームです。ただし、欧米では3D描画が受けることや、アニメ絵風イラストレーターなどもポーズ集がわりに3Dソフトを使ったりするので、そういう事情がある場合にその用途で3Dを使うのはビジネス的に効果的かもしれません。

とりあえず本書では以降、なんらかの理由で3Dの描画を使用したい場合を前提として説明します。


そもそも3Dグラフィックをプログラムする場合とは?[編集]

ゲームで、3次元のCG映像を表示する場合、かならずしもプログラミングする必要はありません。

もし、その被写体を見る視点が一方向だけに固定されている場合なら、w:Blenderなどの、あらかじめ一般に流通している3D-CG作成用ソフトウェアで作ったCGモデルを、その視点の方向から見た場合の画像を、ビットマップ画像などとして出力したものを、作成するゲームに追加して表示すれば十分です。どのCGソフトにも、ビットマップ出力の機能はついています。

高速化のための2次元の処理[編集]

なるべく単なる2次元画像として処理するほうが、パソコンによる処理も速くなります。

例えば、2次元の横スクロールのアクションゲームのように視点が真横からだけに限定されているゲームの場合なら、たとえ「2次元の横スクロールのアクションゲームだけど、リアルさの表現のために、主人公キャラクターや敵キャラや背景画像は3D-CGで作りたい」場合であっても、Blenderなどの3D-CGソフトで作った主人公キャラなどの形状データをビットマップ画像出力した2次元画像データをゲーム中に表示するだけで十分でしょう。

こういうふうに、あらかじめBlender などの3Dソフトで作成しておいた2次元の画像や2次元の動画などで代用する方式のことを、プリレンダリングといいます。「プリ」とは「前」とか事前とか、そういう意味です。

いっぽう、ゲーム内で画像を表示する直前に3D計算して表示する方式のことをリアルタイムレンダリングといいます。


現在のコンピュータ技術では、映像表現については、ビデオカードやグラフィックカードやGPUなど、映像専用の計算処理デバイスが内臓されています。

映像の描画は、なるべく2次元ビットマップ画像としてハードディスクやメモリなどに記憶しておいた画像を呼び出すという方式にしたほうが、それらの画像デバイスにより、並列処理的に高速処理できます。


また、ハードディスクやメモリなどの大容量化をするのは、単にハードの枚数を増やせばいいので比較的に容易ですが、いっぽう、CPUを高速化するのは技術的に難しいことが多いのです。


なので、あまり、ゲーム機で描画のたびに毎回3D計算するのではなく(つまり、リアルタイムレンダリングではなく)、ゲーム制作時の3D計算で画像出力しておいた2次元画像データをゲームのたびに呼び出して高速描画するという方式(プリレンダリングの活用)も、ゲームでは処理速度の向上のために必要になります。

2次元画像データで済む事例[編集]

真横から見る場合だけにかぎらず、たとえば、RPGで、マップ画面を南の上空から北の地面にむかって斜め45度に見下ろすだけ、とかなら、ビットマップ画像で十分でしょう。

このように、視線が1方向に固定されているなら、たとえ斜め方向の視線であっても、また、どんなに写実的な画像であっても、けっしてゲーム中では3Dプログラミングをする必要はないのです。視点さえ固定されていれば、3Dプログラミングをしなくても単なるビットマップでも表示可能です。


どうしてもゲームソフトでわざわざ3D-CGの処理のプログラムをする場合とは、被写体を見る視点が固定されていない場合で、プレイヤーがゲーム中の映像の視点を360度どの角度にも自由な角度に移動できるような機能を搭載したい場合です。


また、パソコンへの処理時の負荷が、2D画像の表示と比較して3DプログラミングはCPUの計算負荷が大きく、このため、パソコンの性能が不足していると3D画像は処理速度を低下させる原因にもなり、また消費電力も大きくなります。

昨今の携帯ゲームなどでは、消費電力の低減も重要な技術です。

なので、本当に3Dプログラミングをする必要のあるものだけ、3Dプログラミングをするのが好ましいでしょう。


実はゲーム機だけにかぎらず、たとえばパソコンで三角関数や対数関数や平方根などの、小数点以下に無限に桁のつづく数学の関数を使う場合も、実はパソコン内部に三角関数表のような近似値の計算結果があらかじめハードディスクに保存されてあり、関数の使用時には、その数表を呼び出して代入しているだけです。その三角関数の数表を作る場合にだけ、パソコン業者などが超高性能なパソコンを使っているのです。

このように数表などで近似計算しないと、処理速度が遅くなっていまい、使い物にならないのです。携帯用の電卓などの平方根の計算も同様です。電卓では、電力の消費も抑えないといけませんから、そのためにもCPUの負荷を減らすことは必要です。


テクスチャー[編集]

テクスチャマッピング

また、市販の3Dゲームでも、実際には、2次元画像と3次元グラフィックとを組み合わせているものも、多くあります。

テクスチャーといって、3次元物体データの指定した面の表面に、画像を貼り付ける技術があります。(w:テクスチャー

これを使うと、あまりにも複雑な形状を3D的に描きたい場合、大まかな形状だけを3Dで作っておいて、細部は2次元画像で描いて表面に貼り付けたほうが、処理が早くなります。


たとえば、物体表面に描かれた細かな模様などは、いちいち、その細かな模様をすべて3D計算していると、とてつもなく膨大な計算量になってしまうので、普通はそういうことはせず、模様を除いた物体の形状だけを3D計算するのが一般的です。

そして、その物体の3D計算の結果を参考に、画面上での模様の表示位置を算出して、まるで物体に模様を貼り付けるような技術によって、計算量を削減するという技術があります。

このような技術が、テクスチャー技術です。


Windowsアクセサリ「ペイント」などの安価な画像製作ソフトで、テクスチャーとして貼り付けるための二次元画像を製作してビットマップ画像などとして出力しておき、

そのビットマップ画像をBlenderなどで3Dモデルにテクスチャーとして貼り付けておけば、

素人のゲームプレイヤーの目からは、あたかも、ゲーム機内で3D計算をしてるかのように見えます。


テクスチャーの利点としてテクスチャーにしたほうが処理は速いのはもちろん、副次的なメリットとして、模様の書き換えなどを表示する場合にも、3D部分は共通ですのでプログラム的にもラクに処理できます。


ザラザラした感じの表面なども、普通はテクスチャーでしょう。もしかしたら、高低差のある物体でも、極端に高低差の小さいデコボコなどは、いっそテクスチャとして画像を表現するべきかもしれません。


また、3Dの人間キャラクターの耳の穴とか、鼻の穴とかは、通常のゲームでは、人体の内部から見る機会はほとんどゼロですし、穴の中を近寄って見る機会もほとんどゼロですので、人体のそういう穴は、けっして細部を正確に3Dモデリングする必要は無いのです。せいぜい、鼻や耳の穴の深さ2センチくらいまで3Dデータを作れば十分であり、その深さ2センチの穴の底に、穴っぽい色をした黒色のテクスチャーを貼り付ければ十分でしょう。


なお、かならずしも3D-CGを使ったからって、それだけで、うまく映像的に表現できるわけではありません。たとえば、光源の位置をどうするか、反射の特性をどうするかなど、適切な設計が必要です。

テクスチャーを使う場合、もし作者であるアナタが、あまり上手に映像設計できないなら、ゲームでは、かえってプレイの邪魔になりかえるので、いっそ単純なポリゴン画像に置き換えるか、もしくは、いっそ2次元で絵を書いてしまいましょう。

アニメーションの必要性[編集]

コンピュータの環境によっては、入力パッドなどからの入力の受付け反応があまり良くない場合もありますので、もし入力があった場合に画面の変化をして、プレイヤーに入力が行われたことを分からせる必要もあります。

パソコンゲームの場合、作者であるアナタのPC環境と、プレイヤーのPC環境は、パソコンでは一般に違います。また、たとえ商用のゲーム機でも、コントローラーが故障する場合もあります(なので、修理のために取り外せて交換できるようになっている)。


たとえば、主人公キャラが平坦で周囲に何もない大きな平原を歩いていても、プレイヤーがもし(入力コントローラーなどの)移動ボタンを押したなら、移動中は画面が一時的に変わらないとイケマセン。

簡単な方法は、主人公を画面の中央に写し、移動中は主人公が歩いている動画を表示することです。

もし そうしないと、プレイヤーが、はたして移動が正常に行われたのかどうか判別できなくなります。

なお、画像でなく足音のような音を音声出力することで移動表現をする方法もありますし、フリーゲームではそれでも充分でしょうが、しかし世の中には難聴など人もいることを念頭に置いてください。市販の大手企業の販売している商業ゲームでは、そういうバリアフリーもできるだけ考慮されています。なので余裕があれば、やはり画像的に入力反応の分かりやすい画面設計をすべきでしょう。


もし、2次元の横スクロールアクションのゲームなら、入力が正常に行われたら普通は、入力キーの方向に主人公が移動したりしますので、(たとえ主人公が歩く動きをしなくても)判定できます。

しかし、3Dでは、主人公はつねに中央にいるか、そもそも主人公が映らないので、2次元のような判定はできません。3Dにかぎらず、2d-RPGなどでも、もし主人公がつねに画面の中央に表示されるなら、こういう歩き動画の工夫は必要です。


さて、迷路の十字路を、左折または右折したい場合もあります。この場合、曲がり始めの10°〜20°くらいの比較的に小さい回転角での画像が必要です、

もし中割り画像が、真ん中の45°の1枚だけだと、十字路の斜め前方にあった とんがった壁が前方に来た時に、はたして斜め右側にあった壁なのか、それとも斜め左側にあった壁なのか、プレイヤーには不明です。なので、最低でも、曲がり始めの10°〜20°くらいの比較的に小さい回転角での画像が中割りに必要です、


このように、ゲームプレイでは、単に1枚の画像の細かさだけでなく、動画で連続的に動きを表現する必要もあります。


もし、主人公を画面に写さずに、また、移動中も特別な画像を出さないなら、そのゲームでは、もはや、周囲に何もない広大な平原のような空間を出すことは不可能です。

なので、もし、そういう画面設計(主人公を画面に写さない、)のゲームの場合は、そういうふうに、そのゲーム内のマップ(地図)を設計する必要があります。

力学計算は行わない[編集]

一般的に流通しているゲームで3D処理のプログラミングを行われている場合、そのようなゲームのほとんどでは、荷重計算や強度計算などの力学の計算は行われていません。

3Dになると映像がリアルなので、てっきり、強度計算などの力学的な計算が行われているように誤解しがちです。

しかし、せいぜい高校物理のレベルの幾何光学の計算しか、行われていません。

こうする理由は、パソコン処理を速くする目的もあります。


荷重計算や強度計算などは、計算を近似しないかぎり、形状がきわめて単純な物体の場合ですら(円柱や立方体や球などですら)、ほとんどの場合は計算に何分~何時間も時間が掛かってしまいます。

しかしゲームでは、なめらかな動画で映像表現する場合などは、画像を1秒間に何十回も描画しなければならない場合もあります。

なので、計算時間の掛かってしまう力学計算は、通常、ゲーム内では行われません。


どうしてもゲーム中で力学的な計算が必要な場合は、近似計算をします。


たとえば、

高校物理で習う放物線運動のように、質点が運動していると近似したり、
あるいは、伸縮・圧縮する物体なら、内部に、高校物理で習うようなバネが入ってると仮定して近似したり、
あるいは、曲げなどをする物体なら、工業高校などで習う強度計算のように、物体の厚さがうすい金属板のような物だと近似したり、

・・・のように、高校レベルか、せいぜい大学1年レベルで計算を近似します。いわゆる「線形近似」で処理します。

いっぽう、科学者のような、より精密な力学計算は、ゲーム機用のコンピュータでは計算に膨大な時間が掛かってしまいかねず、そのせいで、動画表現に向きません。精密すぎる計算は、ゲームには不適切です。


どうしても、高校物理~大学1年レベルを超えた、力学的にリアルな映像が必要なら、

特撮みたいに、現実世界で模型などをつくってビデオ撮影した動画をゲームに入れるとか、

あるいは空想上の物体なら、

開発中に、ほかの大型コンピュータで力学計算しておいて、その計算にもとづく映像を記録しておいて、その記録映像をゲームに取り入れるとか、(なお、このような手法のことをプリレンダ ムービー という)

そういう工夫が必要です。

光学計算[編集]

ゲーム中の光学計算ですら、おそらく近似をされたものが使われてるのが普通でしょう。

光の3D計算というのは、正確な光はw:レイトレーシングという、光線の反射経路を追跡していく方法で再現できますが、しかしレイトレーシングには意外と計算量が必要です。

反射した光が別の物体に当たって反射して、 その光が、また別の物体に当たって反射して、 さらにその光が、また別の物体に当たって反射して・・・・・・、

のように、リアルさを追求すると、限度なく、いくらでも無限に計算量が必要になってしまいます。

「反射光の反射光」という現象のように、2次的な反射光があります。

同様に、

反射光の反射光の反射光という3次的な反射光もあり、
反射光の反射光の反射光の反射光という4次的な反射光もあり、

無限に続きます。

なので、ゲーム中での3D計算による反射光の計算では、計算の負担の軽減のため、1次の反射光や、せいぜい2次の反射光くらいで、とめておくべきでしょう。

もしゲーム中で、よりリアルな高次の反射光のような映像を表示をしたいなら、テクスチャーなどを活用すべきでしょう。

つまり、ゲーム機以外の別のコンピュータで高次の反射光を計算して、その計算結果をビットマップ画像などとして出力しておき、その画像をテクスチャーにするわけです。

物理的な光には、反射光のほかにも屈折光や回折光があるということは、

つまりCG的な光の種類の組み合わせにも、例えば

屈折光の反射光とか、
回折光の反射光とか、
回折光の回折光とか、

そういう色々な組み合わせもあります。それら高次の計算をゲーム機で厳密にしてリアルタイム処理するのは、現状のゲーム機の性能では不可能なので、どうしてもテクスチャーのような、なんらかの擬似的な処理が必要になります。


また、反射や、不透明な物体の影の計算では、物体の表面だけが必要ですので、そのような計算の場合には、物体の内部の光学特性データは不要です。


どうしても物体の内部の光学特性データが必要な場合とは、せいぜい、透明な物体に光が差し込んで屈折するような場合くらいです。

そして、ゲームではたいていの場合、せいぜい高校物理で習う屈折の法則のような、屈折率が物体内で均一なものしか、扱いません。


環境マッピング[編集]

たとえば、中央のオブジェクト(物体)に映りこむ景色を、テクスチャ画像として用意しておけば、環境マッピングになる。

鏡にうつりこむ風景や、金属の光沢などは、正確にシミュレーションして求めるにはレイトレーシングが必要になります。

しかし、レイトレーシングによる計算量は上述のように結構、膨大になりますので、計算量を減らす工夫として、アクションゲームなど速度の要求されるゲームでは、あらかじめ制作者がゲーム機以外の計算機で、ゲーム機中の映像について光学計算のシミュレーションをしておき、そのシミュレーション結果にもとづく2次元テクスチャ画像によって光を表現する場合も多くあります。

このような技術をw:環境マッピングといいます。よく、鏡や光沢のシミュレーションで、上述のようにテクスチャによる代替を行うことについて、「環境マッピング」といいます。

しかし、なにも鏡だけにかぎらず、この技術の本質はレイトレーシング結果をテクスチャで置き換えることですので、照明などによる光の明暗のシミュレ-ションも、環境マッピングのようなテクスチャによる代替が可能です。


環境マッピングの苦手分野として、鏡が移動するような場合は、難しくなります。


また、主人公や敵キャラなどのような移動する物体による光への影響は、追加的な処理が必要になってしまう。なぜなら、静止物だけをテクスチャとしているからである。

なので、たとえば移動する主人公や敵などのキャラクターが、鏡や照明などに近づいた場合は、環境マッピングだけでは表現できない。

別途、移動する物体による鏡面映像などの描画処理を追加する必要がある。


これら「環境マッピング」の技術のように、ゲーム機での光の描画では高速処理のため、あらかじめ基準となる静止物や基準光源などをもとにシミュレーションした2次元画像データをテクスチャなどで作成しておき、もし光源の移動や追加や、移りこむ物体が移動してしまい影が動いてしまったり、鏡に映りこむ風景の変わる等の場合には、追加的に、なんらかの近似的な補正により、基準テキスチャ画像に補正を加えるというような擬似的な方法で、描画を高速処理するという方法も多く用いられる。

1個の平面鏡の中の景色ならレイトレーシングは不要[編集]

景色中にある鏡が平面鏡であり、1つしか鏡のない場合、そもそも、鏡のなかの景色を求めるのには、レイトレーシングをする必要は無い。


中学校・高校の理科や数学などで、1個の平面鏡に映る像の見える計算する公式があったのを思い出そう。


鏡に映る像は、平面鏡では、観測者の反対側に、


たとえば、
           鏡
           ↓
人         |

のように、鏡の前に観測者の人が立ってる場合、

           鏡        像
           ↓        ↓
人         |         入

のように、像は、鏡から観測者の距離が等しい。

そして、鏡のなかでは、左右の向きが反対になる。

自分が鏡を見た場合を思い起こせば、右手に持ってたものは、鏡の中の自分は左手にもっている。


つまり、鏡の中の景色は、単に、鏡の外の景色を左右反転したものになる。


さらに、(別章で説明する)「環境マッピング」などの技術と合わせれば、鏡が固定されている場合、移動物体だけを計算すれば済む。

つまり、平面鏡に写る景色を求めるには、移動物体だけについて、鏡の奥の位置に、左右反転した物体を置けばいい。


ただし、この方法ですら、移動物体のポリゴンの計算量が2倍になってしまうので(鏡の外に置くポリゴンと、鏡の中の世界に置くポリゴンとで、2倍のポリゴン計算が必要になる)、ゲーム中の鏡はどうしても計算量を増大させてしまう。


さて、反射をする道具は、鏡だけではない。金属だって反射をするし、水面だって、いちぶの光を反射をする。ガラス表面も、いちぶの光を反射をする。

しかし、ゲーム中の金属や水やガラスなどのすべての反射物体で、移動物体の映り込みの計算を求めてしまうと、計算量が膨大になってしまうので、たいていのゲームでは、ストーリー上では重要でない物体においては、移動計算の写りこみは無視する。

金属や水面の反射では、光沢だけを描画するという簡略化をするゲームも多い。移動物体の写りこみについては、金属や水面の反射では、省略することが普通であろう。


いっぽう、鏡は、ゲーム中のストーリーで重要な意味をもつ場合が多いので、そのような重要な反射物だけ、光沢以外の移動物体の写りこみなどの反射も計算すればいいのである。そして、たいていの鏡は平面鏡であるので、鏡の中の景色は単に外の景色を左右反転すれば済むので、レイトレーシングすら省略できる。

影の描画[編集]

影の正確な描画には、レイトレーシング的に光の多く当たったところほど明るく描画するという方法が、いちばん正確である。

しかし、その理想をそのまま実行するには、光の反射や散乱などをたくさん計算しないとならなくなるので、処理が重くなるので、レイトレーシング的な影の描画方法は、ゲームでは、あまり使われない。


そこでゲームでは、よく妥協案として、光源から出たそれぞれの光線ごとに、光源に一番近い物体によって遮られた(光源の)反対側を暗くする、という処理で近似することも多い。例えるなら、隠面処理のZバッファ法を、明暗の表現に応用するような手法である。

まるで影絵のように、光源と遮蔽物の反対側を、暗くすればいいのである。


被写体ごとに、光源に一番近い面だけを明るく描画し、光源に遠い部分を暗くすれば、影の表現になる。

この計算のためには、当然、被写体ごとに光源からの距離を、計算しなければならない。


さて、複数の光源がある場合でも、それぞれの光源の反対側を暗くすればいい。

なお、それぞれの光源による、影と影とが重なったところは、さらに暗くなる。


細かなノウハウ[編集]

遠景用のポリゴンデータ[編集]

たとえば、カメラの遠くにある物体を、細かく描画しても、無駄になります。

なので、遠くにある物体は、ポリゴン数を減らすという、アイデアもあります。


たとえば、遠くにある建物や地形などを、細かく描画しても、無駄になってしまうでしょう。


なので対策として、ポリゴンデータを2種類用意しておけばいいのです。近景として眺めた場合のポリゴンデータと、遠景として眺めた場合のポリゴンデータの2種類を、制作時に用意しておき、ゲームデータとしてゲーム機に組み込んでおくのです。


近景用では、ポリゴン数を多めにデザインしておくのです(いわゆる「ハイポリ」)。

いっぽう、遠景用では、ポリゴン数を少なめにデザインしておくのです(いわゆる「ローポリ」)。


また、人物でもワキ役のポリゴン数を減らすことは、よく行われます。


技術的な背景として、21世紀のパソコンではハードディスクやメモリに記憶するデータ量と、CPU(またはGPU)の処理速度とのトレードオフとの問題です。

高速化のためには、ハードディスク & メモリの使用量を増やしてでも、CPUやGPUの負荷を減らす必要があるので(現在のハードウェア技術では、それが人類のパソコン設計の限界)、たとえポリゴンモデル設計時の作業負担が増えてでも(※ 設計時にハイポリとローポリの2回のデザイン仕事が必要になるので)、遠景のポリゴンを簡略化することでCPUの負担を減らすという工夫が、市販の3Dゲームなどでも、よくあります。


3Dグラフィックのプログラミング[編集]

現代では一般に、3DのグラフィックにはDirectXOpenGLなどのAPIが利用されます。詳しくはOpenGLなどを参照してください。


しかし、実は、これらのソフトを使わなくても、3Dプログラミングは可能です。

実際、1980年代のマイコンBASICなどのプログラム入門書などを読むと、中学生~高校生向けに3Dプログラミングのソースコードが書かれていたりする場合もありました。その程度の初歩的な知識でも、3Dプログラミングは可能です。


近年のインターネット上には、「3Dプログラミングには、理系の大学専門レベルの高度な数学の知識が必要! たとえば四元数が~~(以下略)」などというタワゴトがありますが、それは間違った情報ですので、だまされないようにしてください。

もちろん、数学の知識があるのに越したことはないですが、しかし、高校レベルの三角関数に毛が生えた程度の知識でも、3Dプログラミングは可能ですし、1980年代のマイコンBASICのブームの時代から、そういう高校数学レベルで分かる3Dプログラミングの入門書は存在しています。


とはいえ、いまさら3Dソフトをゼロから自作するのは(個人でも不可能ではないが)調べることも多く手間が掛かるので、たいていは、DirectXやOpenGLなど既存のツールを使って、3Dグラフィックを表示するためのプログラム作成します。


下記の3Dプログラミングの解説でも、いろいろな数式が出てきますが、数式のひとつひとつは、高校レベルのベクトルや行列、三角関数といった数式です。

行列は、高校カリキュラムが数年ごとに変更するので、年代によっては高校で習ってない場合もありますが、ここでいう行列とは単に、ベクトルを並べたものです。

行列の計算法について詳しくは高等学校数学C/行列を参照してください。

ケーススタディ[編集]

カメラの配置[編集]

3Dゲームを作るときにはカメラの動作を決める必要があります。2Dの表現ではキャラクタを上から見る、斜め上から見るなどの表現がなされますが、それらはどれも場面をある1方向から見る表現でした。一方、3Dで場面を作るときには、それらはあらゆる方向から見る事が出来るので、見る人の視点によって描画される内容を変更する必要があります。

簡単な表現では、カメラの位置はプレイヤーが動かすキャラクタの場所に固定します。このとき、プレイヤーキャラクタが画面内に映らなくなる表現とカメラをプレイヤーキャラクタの後方に配置して、プレイヤーキャラクタを画面内に含める表現があります。前者の表現はFPS(First-Person shooting: 1人称シューティング)でよく用いられます。

基本的に3Dでの描画を行うには、それぞれの物体に対して座標を与え、それを適切な角度から2Dへの射影を行う事でなされていました。このとき、3Dの描画にOpenGLを使うとすると、各物体の座標の与え方にいくつかの制限が加わります。例えば座標の原点は常にカメラの位置に固定される、画面上方向は常にy座標とする、などです。

幸いにもgluLookAt関数を利用することで、この制限を乗り越えることができます。gluLookAt関数は、カメラの位置、カメラが向いている方向、上方向の3つの3次元ベクトルを用いて指定する関数で、カメラの位置を変更するのと同じ効果をもたらす行列を作成し、現在選択されている行列にかけます。通常の状態ではカメラの位置(0,0,0)、カメラが向いている方向(0,0,-1),上方向(0,1,0)となり、この時かけ算される行列は単位行列に等しくなります。 gluLookAtの行列は次で与えられます。

まず、

で表される行列で、座標の方向を調節します。ただし、

,, , ,

を用います。更に、カメラの位置を動かす為に、

glTranslated(-eyex, -eyey, -eyez);

を実行します。

ここでは、上の変換のうちで座標の方向を変更する行列について説明します。この行列の導出にはいくつかの前提が必要となります。まず、変換後の座標系ではカメラは-z方向を向いています。またこの時、画面上方向は必ずy方向になります。これらはOpenGLの座標の取り方から来る制限です。ここではもう1つの制限として、gluLookAtで得られる行列が直交行列であることを仮定します。直交行列の各行と列は、お互いに直交する単位ベクトルとなり、直交行列の逆行列は元の行列の転置行列になります。詳しくは線型代数学を参照して下さい。

行列を求める方針として、に変換し、に変換するような行列を探します。このような行列をPとおくと、仮定からPは直交行列なので、

より、

が成り立ちます。最後の式は具体的に計算できて、

が得られます。同様にして、

が得られ、求めたい行列のうち6つの要素が得られたことになります。Pが直交行列という仮定を用いると、は、に直交する必要があります。このようなベクトルはに比例する必要があります。ここでは右手系の座標系を保つために、

と取ります。これで元の変換行列が得られたことになります。

プレイヤーが動かすキャラクタの位置と向いている方向を表す構造体をcameraとし、その値を次のようにおくと(cはcamera型の変数で、この変数がプレイヤーキャラクタの位置と方向を保持しているものとします)、

typedef struct {
  double x,y,z,dx,dy,dz;
} camera;
camera c;

カメラの位置をプレイヤーキャラクタの位置に変更する方法は次のようになります。

void set_camera(){
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluLookAt(c.x,c.y,c.z,c.x+c.dx,c.y+c.dy,c.z+c.dz,0,0,1);
}

ただし、ここでは最後の引数(0,0,1)で、上方向がz方向であると定義しました。

上の変換では、カメラの位置を変更したのですが、これだけだと変更された後の位置を中心として、長さ2で表される立方体の中の物体しか描画されません。これはもともと投影行列が単位行列だった時には、原点を中心として長さ2の立方体内の物体しか描画されないことと対応しています。より遠方の物体を描画するには、glOrtho, glFrustumの両関数を利用することができます。一般的なゲームでは"遠方のものは小さく見える"といった表現がなされるので、ここではglFrustumを用います。glFrustumの引数は状況によりますが、x,y方向に関係する引数を大きく取ると視界が広くなり、z方向の座標を大きく取ると遠くのものまで見えるようになります。ここでは

glFrustum(left,right,bottom,top,near,far);

をgluLookAtの1つ前にいれます。

実際にこの関数を使ってプレイヤーキャラクタが動く様子を書くことができます。処理の様子は、

int main(){
init_gl();
init_camera();
while(1){
  set_camera();
  draw_scene();
  swap_buffers();
  check_fps();
}
return 0;
}

のようになります。set_camera以外の関数は説明していないので、以降で説明します。

  1. init_glは、OpenGLの初期化を行う関数です。この関数は使っている環境によりますが、glutInit (GLUT) , SDL_SetVideoMode (SDL) などが対応する関数です。
  2. init_cameraは、プレイヤーキャラクタの位置を設定する関数です。cをグローバル変数としておけば、camera型の変数を、引数として渡す必要は無くなります。
  3. set_cameraは先程説明した関数です。ここではカメラの位置は定数なのですが、後にカメラの位置を動かす場合も扱うので、ループ内に入れました。
  4. draw_sceneは画面に現れる物体を描画する部分です。ここではいくつかの三角錐を配置しました。
  5. swap_buffersは、glが描画した内容を画面に表示する関数です。glutSwapBuffers (GLUT) , SDL_SwapBuffers (SDL) などが用いられます。
  6. check_fpsは、処理にかかった時間を計算し、画面の書き換えのタイミングと次の処理のタイミングが合うようにする関数です。usleep(環境依存), SDL_Delay (SDL) などが用いられます。

次の図は、カメラの位置を動かしながら描画を行ったものです。元々遠くに見えていたものが近づくにつれて大きくなる様子がわかります。

Gl3dgame view1.png
Gl3dgame view2.png
Gl3dgame view3.png

具体的には画面中に見える白い物体はそれぞれ三角錐で、中心の座標は(0,0,0),(3,1,0),(3,4,3),(5,-2,1)となっています。更にカメラの位置は(10,0,1)であり、カメラが向いている方向はいずれの場合も-x方向です。

ここで、init_cameraとdraw_sceneの内容を紹介します。init_cameraは、カメラの位置と方向を定める構造体cに、初期値を与える関数です。ここでは次のようにしています。

static void init_camera(){
        c.x = 10;
        c.y = 0;
        c.z = 1;
        c.dx = -1;
        c.dy = 0;
        c.dz = 0;
}

単にカメラの座標を(10,0,1)とし、方向を-x方向に定めているだけです。ここでz座標が0でないのは、FPSでカメラの位置がキャラクタの顔の辺りにおかれることを意識したものです。c.zを0とすると地面を這うような表現になります。

draw_sceneは、次のような関数です。

static void draw_scene(){
        glClear(GL_COLOR_BUFFER_BIT);
        watch_from_camera();
        draw_cone(0,0,0);
        draw_cone(3,1,0);
        draw_cone(3,4,3);
        draw_cone(5,-2,1);
}

glClearは、画面の表示をクリアする関数です。watch_from_cameraとdraw_coneはそれぞれ次のように与えています。

static void watch_from(double x, double y, double z, double dx, double dy, double dz){
        glMatrixMode(GL_PROJECTION);
        glLoadIdentity();
        glFrustum(-1,1,-1,1,0.9,50);
        gluLookAt(x,y,z, x + dx,y+dy,z+dz, 0,0,1);
}
static void watch_from_camera (){
        watch_from(c.x, c.y, c.z, c.dx, c.dy, c.dz);
}

ここで、watch_fromが視点変換を行う関数の本体であり、watch_from_cameraは、引数を与えることを目的とした関数です。ここでは

  1. カメラがただ1つであり
  2. その名前はcであり
  3. それはグローバル変数である

という仮定をおいています。更に、draw_coneは、

#define A (glVertex3f(x,y,z));
#define B (glVertex3f(x+0.5,y,z));
#define C (glVertex3f(x,y+0.5,z));
#define D (glVertex3f(x,y,z+0.5));
static void draw_cone(double x, double y, double z){
        glBegin(GL_TRIANGLES);
        A B C;
        A B D;
        B C D;
        A C D;
        glEnd();
}
#undef A
#undef B
#undef C
#undef D

としています。頂点と面を指定して多角形を書くときには、頂点と面のそれぞれをGLfloat型と、GLint型の2次元配列を使う方が普通です(例えばRed Book3章)。ここでは簡単のためマクロを使いました。

ここまででカメラの位置を指定する方法を解説しました。ここからはカメラの位置を機器の入力を受け付けて変更する方法について述べます。

実際には入力を受けてカメラの位置をどのように動かすかは、例をアクションゲームに限っても、個々のゲームによって変化します。例えば、

  1. 常にプレイヤーキャラクタの後ろにつく[1]
  2. プレイヤーキャラクタの位置とカメラの位置を別に用意し、それぞれを別に動かせるようにする[2]
  3. プレイヤーキャラクタの後ろにつくが、キャラクタが反転したときや移動したときの動作に自由度を残す(即座にキャラクタの後ろに回るわけでないので、キャラクタの正面が見えることがある)[3]
  4. 場面毎にカメラの位置を固定する[4]

ここでは簡単のため、1番目の選択肢である"プレイヤーキャラクタの後ろにつく"を採用します。これは、カメラの位置とキャラクタの位置が同一であるか、簡単な変換で導出できる関係にあるためです。

既に2Dのゲームプログラミングを通して、何度か入力機器の取扱いについて見て来ました。ここでは入力機器を扱う方法としてSDLを使います。SDLは入力機器の初期化と、2D,3Dの描画を扱うための簡単なライブラリで、多くの(主に個人製作の)ゲームで利用されています。

SDLでは機器の入力はイベントとして扱われます。イベントを供給する元はシステムによって様々ですが、X(Linux他)や、DirectInput (Windows) が用いられます。SDLがこれらのシステムからイベントを得ている方法を見るには、SDL-x.x.x/src/video以下の各ディレクトリを見ることが必要です。ここでは詳細を追求せずに、SDLの関数を使うことにします。

実際にイベントの監視を行うには、メインループの中で、

while(SDL_PollEvent(&e))
  process_event(&e);

などとします。ここで、eはSDL_Event型の変数で、main関数内で定義します。実際にイベントを扱うのはprocess_event関数で行います。SDLが扱うイベントは、キーボード、マウス、ジョイスティックなどの入力機器からの要請の他に、スクリーンからのexposeイベントやresizeイベントがあります。これらは使っていたウィンドウが他のウィンドウで隠された時や、扱うウィンドウの大きさを変更したときに供給されるイベントです。これらの様々なイベントを扱うため、SDL_Event構造体には、

Uint8 type;

という要素が含まれています。これは各々のイベントの種類を表す要素で、process_event内ではこの値に従って処理を分ける必要があります。具体的には

static void process_event(SDL_Event *e){
  switch(e->type){
    case(SDL_KEYDOWN):
      cb_keydown((SDL_KeyboardEvent *)e);
      break; 
    case(SDL_KEYUP):
      cb_keyup((SDL_KeyboardEvent *)e);
      break; 
    default:
      break; 
  }
}

cb_keydown(up)の関数では実際に押された(離された)キーを取得します。このときにはSDL_KeyboardEvent内の、

e->keysym.sym

要素を利用します。詳しくは、SDLのインストール先から、SDL/SDL_keyboard.hや、SDL/SDL_keysym.hなどを参照してください。cb_keydown(up)内での具体的な処理は対応するpressed関係の変数を書き換えることです。この処理は2Dの時の例と同じなので省略します。

ここまででキーが押されているかどうかを知る事が出来るようになりました。ここからは具体的なカメラの移動を見ていきます。ここではset_camera関数内で、カメラの移動を行います。カメラの位置と方向はc内に記録されているので、この関数ではpressed関係の変数の値を見て、カメラの方向を変更する処理が必要になります。

実際に行う処理は、カメラの位置を変更する処理と、カメラの方向を変更する処理に分かれます。ここではカメラの位置を変更する処理を先に扱います。

FPSでは、カメラとキャラクタの移動は、次のように行われます。

  1. "前"を選ぶとキャラクタはカメラが向いている方向に進む
  2. "後ろ"を選ぶとキャラクタはカメラが向いている方向と反対に進む

ここでは"前"を表すキーとして上キーを使い、"後ろ"を表すキーとして下キーを用います。具体的なset_cameraは次のようになります。

static void set_camera(){
  if (up_is_pressed()){
    c.x += a*c.dx;
    c.y += a*c.dy;
  } else if (down_is_pressed()){
    c.x -= a*c.dx;
    c.y -= a*c.dy;
  } else if ()
    // 回転のための処理
}

ここで、aはキャラクタの移動速度を調整するための定数です。実際には人間が地面を蹴って移動するとき人間が得るのは力積なので、キャラクタの移動では位置ではなくキャラクタの速度を変更するべきです。ここでは簡単のため移動にかかる時間は無視できるものとしました。力積については高等学校理科 物理Iを参照してください。

次に、キャラクタの方向を変える処理について説明します。ここではカメラの方向を3次元ベクトルで保存しているのですが、説明の都合上、方向を&thetaと&phiを使って表します。ここで、&thetaは、z軸方向とカメラの方向がなす角、&phiは、x軸方向と、カメラの方向ベクトルをxy平面に射影したベクトルがなす角とします。

通常キャラクタの方向を変更するときには&phiだけを変更します。ただし、キャラクタの視点に立って辺りを見回す表現(主観視点と呼ばれる)では、&thetaも含めて変更する必要があります。ここでは&phiだけを変更します。具体的には左回転をするときには、

phi += b;

右回転では

phi -= b;

とします。ここで、bはキャラクタの回転速度を表す定数です。

キャラクタの方向を表す3次元ベクトルと&theta, &phiは次の三角関数で結ばれています。

逆の変換は

となります。これらの値を使って3次元ベクトルと方向を表す角の変換を行うことができます。

1度の回転の角度が十分小さいときには、

static void turn_left(){
c.dx -= m*c.dy;
c.dy += m*c.dx;
normalize(c);
}

で左回転を、

static void turn_right(){
c.dx += m*c.dy;
c.dy -= m*c.dx;
normalize();
}

で右回転を表すこともできます。ここで、mは定数であり、normalize関数は、cの方向ベクトルを正規化する関数としました。これは、回転の角度が小さいとき、回転による方向ベクトルの変更を元のベクトルに直交するベクトルで近似できることを利用した変換です。方向角を使わないで回転を表現したい場合に利用するとよいでしょう。

これらの関数を用いると、set_camera内のif - else if文に、

} else if (left_is_pressed()){
  turn_left();
} else if (right_is_pressed()){
  turn_right();
}

を付け加えることになります。

具体的な例:sdl_3d_jump[編集]

ここまでで3Dの座標を設定し、定義された物体をプレイヤーキャラクタの視点から観測する方法についてまとめました。例えば3Dのアクションゲームではこれらの手法に加えて重力を与えて、キャラクタが下向きに落下するようにしたり、キャラクタをジャンプさせるという作業が必要になります。しかし、これらの手法は基本的に2Dの場合と変わらないので、ここでは詳しく述べません。

実際には、上で述べたカメラの動かし方と、xjumpなどの2Dアクションの手法を用いて、3Dにおけるxjumpの対応物を作ることができます。ここでは、実際にそれを作成した場合について解説します。

xjumpの対応物を作るために最低限必要な要素は次のようになります。

  1. 床を作る
  2. キャラクタが空中にいるとき、キャラクタが落下するようにする
  3. 床の上にキャラクタがいるときには、キャラクタは落下しない

他に、本来のxjumpでは

  1. ジャンプに成功した枚数を表示する
  2. キャラクタの状態によって、キャラクタの画像が変化する

などいくつかの要素があります。ここでは簡単のため、

  1. 枚数は表示しない
  2. キャラクタは三角錐で代用

などとして進めます。

注意
DirectXの場合と違い、OpenGLには3Dのデータを読み込むための関数は存在しません(DirectXにはXファイルと呼ばれるファイル形式がある)。後に、外部ライブラリを用いて3Dデータを読み込む手法について説明します。

ここからは先に述べた3つの要素について解説します。xjumpでは、ジャンプによって飛び移るための床は、乱数を用いて生成していました。ここでは簡単のため床の位置は定数の2次元配列で与えることにします。更に、床の大きさを固定することにすると、各々の床を得るために必要な情報は、床のうちの任意の1点です。ここではx座標とy座標が最も小さい部分とします。これらの情報は、例えば、

double planes [N_PLANES][3] = {{0,0,2},{4,0,4}};

のように与えることができます。

次に、キャラクタが落下する処理については、カメラの位置を動かす処理で、

c.z -= vz;
vz -= g;

などとします。gはここでは下向きの加速度を表す定数です。これらの処理は2Dの場合と同じなので詳しく述べません。

最後に、キャラクタが床の上にいるかを判定するためには、次のような関数を使います。

static int on_the_ground(){
  int i = 0;
  while (i < N_PLANE){
      if (
         (PL_X < planes[i][0] + 9.5) && (PL_X > planes [i][0] +0.5)
          &&(PL_Y < planes[i][1]+6.5) && (PL_Y > planes [i][1]-0.5)
          &&(PL_Z < planes [i][2]+1.0)&&(PL_Z > planes[i][2]-0.5)
      )
     {
         return 1;
     }
         ++i;
}
  ...
}

if文で行っていることはキャラクタの位置(PL_X,PL_Y,PL_Zで与えられる)が各々の床の範囲内にあるかどうかを確かめる作業です。3Dの時の違いは、x座標とy座標の2つについて判定が必要になった点だけです。また、実際にキャラクタが床にいるときに、キャラクタのv_zを0にするなどの処理も行っていますが、2Dの時と変わらないので省略します。

これらの手順で作ったプログラムを仮に3d_xjumpと呼びます。実際に筆者がGLの初期化にSDLを利用してこれを作成してみたところ、300行程度で収まりました。ただし、ここではカメラに回転を行わせず、常にx座標正の方向からキャラクタを見る視点にしました。ここでスクリーンショットをいくつか載せます。

3d xjump beginning.png
手前に見える三角錐を操って空中のボードに飛び移っていきます。上の方に多くのボードが見えています。
3d xjump first floor.png
最初の1段に飛び乗った所です。画面中央に次に飛び移れそうなボードが見えています。
3d xjump middle floor.png
ある程度進んでから撮ったショットです。画面奥に見える2枚のボードの間はかなりの距離があり、飛び移るのが難しい難所となっています。
3d xjump hard stuff.png
先程見えていた2枚のボードの間に来た場面です。右端に見えているのがこれから飛び移るボードですが、距離を正確に読むのが難しいポイントです。
3d xjump falling.png
コースの全景が見えるポイントから撮ったショットです。実際には画面中央上寄りに見えるボードから飛び降りながら撮りました。画面中央奥手に見える2枚のボードが先程の難所で、画面中最も左下に見えるボードが1枚目のボードです。

ここまででキャラクタやカメラの位置を動かす方法について説明しました。ここからは、より複雑な物体を表示する方法について述べます。ただし、複雑な物体を扱うときでもカメラやキャラクタの位置を動かす方法はこれまでと同様です。

物体の表示[編集]

ここでは複雑な物体を表示する方法について説明します。既に、OpenGLで頂点を用いた物体の描画について説明しました。また、カメラ配置の例では、三角錐を作成しました。実際にはより複雑な物体を表示することが求められます。例えば後に取り上げるSuperTuxKartでは、カートに乗ったペンギンなどが画面に表示されます。また、それらのカートが走るコースも、表示する必要があります。

これらの描画を行うには、3Dのデータを作成するためのソフトウェアが必要となります。このソフトウェアはモデラと呼ばれ、3Dのゲーム開発をする上で重要なソフトウェアです。モデラ自体はゲームだけでなく、3Dのモデルを作成するときには常に利用されます。

フリーで使用できるモデラとしてはblenderが有名です。このソフトは、Windows, Mac OS, Linuxなど各種のプラットフォーム上で利用できるソフトウェアで、先程のSuperTuxKartでは実際に使用されているようです。ソフトの使い方はblenderなどを参照して下さい。

モデラを使って作成されたデータは、3Dデータとして保存されます。これは頂点の位置や、面を塗る色などを指定しており、これを読み取ることでモデラで作ったデータを画面上に表示する事が出来ます。

残念ながらこれらのデータには、標準化されたものがなく、データ形式はモデラごとに変化します。各種の形式を読み取るためにはそれらに対応するライブラリを使うのが簡単です。SuperTuxKartでは、PLIBを使っています。PLIBは、Windows, Mac OS, Linuxなどで動作する3Dプログラミングの為のライブラリです。SuperTuxKartではデータの受渡しに.acファイルを使っていますが、このファイル形式はblenderとPLIBの双方でサポートされています。

注意
最近3Dデータの共通フォーマットとしてCOLLADASCEから提唱されたようです。

実際にはPLIBを利用して、カメラの配置など3Dプログラミングに必要な作業を行う事も出来ます。しかし、ここではPLIBの機能には深入りせず、与えられた3Dモデルのファイルを表示するプログラムを作成するに留めます。このプログラムは、PLIBのサンプルであるplib_example( )の、src/ssg/tux/tux_example.cxxとほぼ同じ内容です。

注意
ここではOpenGLを用いるためにGLUTを利用しますが、PLIB自身もOpenGLの設定を行う機能を持っている様です。
#include <plib/ssg.h>
#include <GL/glut.h>
ssgRoot *root = NULL;
static void display(){
  glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
  ssgCullAndDraw(root);
  glutSwapBuffers();
}
int main(int argc, char **argv){
  glutInit(&argc, argv);
  glutInitDisplayMode(GLUT_DEPTH);
  glutCreateWindow("aaa");
  glEnable(GL_DEPTH_TEST);
  data_init();
  camera_init();
  glutDisplayFunc(display);
  glutMainLoop();
}

ここまでが、PLIBとGLUTを用いるときの基本的な描画を行う部分です。今までと違うのは、物体の描画にssgCullAndDraw関数を用いていることです。PLIBは3DモデルのデータをssgRootクラスを用いて管理します。ssgCullAndDraw関数は3Dモデルのデータを実際に描画する関数です。ここでは描画する3Dモデルに対するポインタをrootとしました。実際にデータをrootに与える部分はdata_init関数としており、次のように与えられます。

static void data_init(){
  root = new ssgRoot;
  ssgEntity *snowman = ssgLoadAC("snowman.ac");
  root->addKid(snowman);
}

ただし、ここでは"snowman.ac"というACファイルが同じディレクトリ内にあるものとしました。ここまでで物体の描画は行われますが、物体が実際に見えている事を知るために、カメラの位置を動かす必要があります。その作業はcamera_init関数で行います。

static void camera_init(){
  sgCoord campos;
  sgSetCoord(&campos, 0, -10,0,0,1,0);
  ssgSetCamera(&campos);
}

ここではカメラの位置を(0,-10,0)とし、カメラの向きを(0,1,0)としました。何も設定しないとファイルから読み出された物体の位置は(0,0,0)となるので、これで設定は完了です。

Game programming plib snowman.png

ここで作成したacファイルは球と円柱だけを用いた物です。より複雑な物体を作るにはblenderなどを参照して下さい。

具体的な例[編集]

glTron[編集]

w:en:GLtron

Neverball[編集]

w:en:Neverball

SuperTuxKart[編集]

http://supertuxkart.berlios.de/

手書きアニメ調の3D[編集]

参考文献[編集]

  1. ^ HALOマイクロソフト)、バイオハザード4カプコン)など。
  2. ^ スーパーマリオ64任天堂)など。
  3. ^ ゼルダの伝説 時のオカリナ(任天堂)など
  4. ^ バイオハザード(カプコン)など。