本章では、3Dオブジェクトに実行する変換処理を専門的に担当するクラスを作成し、個々の3Dオブジェクトに、その変換処理専用クラスのインスタンスを独自に保持させ、3Dオブジェクトに対する変換は、そのインスタンスを通して行うようにする。すなわち、本章の目的は基本的な変換処理のオブジェクト化である (「オブジェクト化」の「オブジェクト」は、いわゆる「オブジェクト指向」で使われる意味での「オブジェクト」である)。
# Code1
以下の図に示される、3つのオブジェクトの一体化した運動を例にとろう。
Stick1、Stick2、Stick3は形状は同じであるが大きさの異なる3つのオブジェクトで、図4に示される親子関係がある (Stick1が一番上の親)。図5は以下のプログラムの実行結果で、この3つのオブジェクトを一体化した状態である。
[Code1] (実行結果 図5)
Matrix4x4 localStick3 = TH3DMath.GetTranslation4x4(c_attachPos_Stick3);
Matrix4x4 localStick2 = TH3DMath.GetTranslation4x4(c_attachPos_Stick2);
Matrix4x4 localStick1 = Matrix4x4.identity;
Matrix4x4 worldStick3 = localStick1 * localStick2 * localStick3;
Matrix4x4 worldStick2 = localStick1 * localStick2;
Matrix4x4 worldStick1 = localStick1;
Stick3.SetMatrix(worldStick3);
Stick2.SetMatrix(worldStick2);
Stick1.SetMatrix(worldStick1);
1行目の
c_attachPos_Stick3 は Stick3のアタッチポジションを表す
Vector3型の定数で、具体的には このアタッチによって Stick2の青い部分の上に Stick3の青い部分が重なった状態になる。2行目の
c_attachPos_Stick2 は Stick2のアタッチポジションを表す
Vector3型の定数で、このアタッチも同様に Stick1の緑の部分の上に Stick2の緑の部分が重なった状態になる (図5)。
# Code2
続いて、この3つのオブジェクトに運動を行わせる。
[Code2] (実行結果 図6)
i_degStick3 += 16;
Matrix4x4 rotStick3 = TH3DMath.GetRotation4x4(i_degStick3, Vector3.up);
Matrix4x4 traStick3 = TH3DMath.GetTranslation4x4(c_attachPos_Stick3);
Matrix4x4 localStick3 = traStick3 * rotStick3;
i_degStick2 += 4;
Matrix4x4 rotStick2 = TH3DMath.GetRotation4x4(i_degStick2, Vector3.up);
Matrix4x4 traStick2 = TH3DMath.GetTranslation4x4(c_attachPos_Stick2);
Matrix4x4 localStick2 = traStick2 * rotStick2;
i_degStick1 += 1;
Matrix4x4 localStick1 = TH3DMath.GetRotation4x4(i_degStick1, Vector3.up);
Matrix4x4 worldStick3 = localStick1 * localStick2 * localStick3;
Matrix4x4 worldStick2 = localStick1 * localStick2;
Matrix4x4 worldStick1 = localStick1;
Stick3.SetMatrix(worldStick3);
Stick2.SetMatrix(worldStick2);
Stick1.SetMatrix(worldStick1);
このプログラムは、3つのオブジェクトがそれぞれの位置で回転を行うだけのものである。Stick1は初期状態の位置から動いていないので、その位置でy軸周りに回転を行う。
Stick2は親オブジェクトStick1の緑の部分に、自身の緑の部分が重なるようにアタッチされるが、Stick2の回転はその緑の部分を中心として行われる (Stick2の回転軸は緑の部分の中央を通るy軸に平行な軸である)。
Stick3は親オブジェクトStick2の青い部分に、自身の青い部分が重なるようにアタッチされるが、Stick3の回転はその青い部分を中心として行われる (Stick3の回転軸は青い部分の中央を通るy軸に平行な軸である)。
プログラム中の変数
i_degStick1、
i_degStick2、
i_degStick3 は Stick1、Stick2、Stick3の回転角度を表すインスタンス変数で、それぞれ毎フレーム $1$、$4$、$16$ずつ増加するので Stick1は毎フレーム $1$°ずつ、Stick2は毎フレーム $4$°ずつ、Stick3は毎フレーム $16$°ずつ回転をすることになる。
# Code3
次に、キー操作によって3つのオブジェクトのそれぞれに対して 回転/停止 の切り替えが行えるように、プログラムを改造する。
[Code3] (実行結果 図7)
if (Input.GetKeyDown(KeyCode.J))
{
i_switch_Stick1 = !i_switch_Stick1;
}
else if (Input.GetKeyDown(KeyCode.K))
{
i_switch_Stick2 = !i_switch_Stick2;
}
else if (Input.GetKeyDown(KeyCode.L))
{
i_switch_Stick3 = !i_switch_Stick3;
}
if (i_switch_Stick3) { i_degStick3 += 16; }
Matrix4x4 rotStick3 = TH3DMath.GetRotation4x4(i_degStick3, Vector3.up);
Matrix4x4 traStick3 = TH3DMath.GetTranslation4x4(c_attachPos_Stick3);
Matrix4x4 localStick3 = traStick3 * rotStick3;
if (i_switch_Stick2) { i_degStick2 += 4; }
Matrix4x4 rotStick2 = TH3DMath.GetRotation4x4(i_degStick2, Vector3.up);
Matrix4x4 traStick2 = TH3DMath.GetTranslation4x4(c_attachPos_Stick2);
Matrix4x4 localStick2 = traStick2 * rotStick2;
if (i_switch_Stick1) { i_degStick1 += 1; }
Matrix4x4 localStick1 = TH3DMath.GetRotation4x4(i_degStick1, Vector3.up);
Matrix4x4 worldStick3 = localStick1 * localStick2 * localStick3;
Matrix4x4 worldStick2 = localStick1 * localStick2;
Matrix4x4 worldStick1 = localStick1;
Stick3.SetMatrix(worldStick3);
Stick2.SetMatrix(worldStick2);
Stick1.SetMatrix(worldStick1);
1~12行目までがキー操作のコードで、この部分で使用されている変数
i_switch_Stick1、
i_switch_Stick2、
i_switch_Stick3 は Stick1、Stick2、Stick3の 回転/停止 を切り替えるために用いられる
bool型インスタンス変数である。例えば、Jキーを押すと
i_switch_Stick1の値が反転するが(1~3行目)、これによって Stick1が回転したり停止したりする。
回転/停止の切り替え処理は単純である。具体的には、
i_switch_Stick#の値が
trueのときには回転角度を増加させ、
falseのときには回転角度の更新を行わない。例えば、Stick3の場合
i_switch_Stick3が
trueのときは14行目において
i_degStick3が、毎フレーム 16ずつ増加する。15行目の
rotStick3 は Stick3を角度
i_degStick3だけ回転させる回転行列であるが、
i_switch_Stick3が
trueのときは毎フレーム
i_degStick3の値が変化するので
rotStick3の内容も変化し、その結果 Stick3は回転状態になるが、
falseのときは
i_degStick3の値に変化が起こらないので、その場合
rotStick3の内容は毎フレーム同じ内容であり、結果として Stick3が停止した状態になる。
Stick1、Stick2の回転/停止についても同様である。
ここで、一番上の階層のオブジェクトStick1の回転を止めたしよう。Stick1の回転/停止は Jキーで操作でき、停止の際には、
i_switch_Stick1の値が
falseとなり、24行目の
ifブロックにおける
i_degStick1の更新処理が行われなくなる。
if (i_switch_Stick1) { i_degStick1 += 1; }
Matrix4x4 localStick1 = TH3DMath.GetRotation4x4(i_degStick1, Vector3.up);
このとき、
i_degStick1を使用する25行目の処理は、
i_degStick1の値が変化しないため
localStick1には毎フレーム全く同じ行列が計算されてセットされる。
さらに、続けて2番目の階層のオブジェクトであるStick2の回転も停止したとしよう。Stick2の回転/停止は Kキーで操作でき、停止の際には、
i_switch_Stick2の値が
falseとなり、19行目の
ifブロックにおける
i_degStick2の更新処理が行われなくなる。
if (i_switch_Stick2) { i_degStick2 += 4; }
Matrix4x4 rotStick2 = TH3DMath.GetRotation4x4(i_degStick2, Vector3.up);
Matrix4x4 traStick2 = TH3DMath.GetTranslation4x4(c_attachPos_Stick2);
Matrix4x4 localStick2 = traStick2 * rotStick2;
Stick2の回転を停止した場合、
i_degStick2の値が変化しないため、20行目の
rotStick2には毎フレーム全く同じ行列が計算されてセットされる。21行目の
traStick2にもアタッチポジションへ平行移動する同じ行列が毎フレームセットされるので、それらの積である22行目の
localStick2にも毎フレーム同じ行列が計算されセットされることになる。
さらに、Stick1と Stick2を停止している状態では、今見てきたように、それらのローカル行列である
localStick1、
localStick2が毎フレーム同じ内容であるため、以下のワールド行列の計算においては、27行目、28行目に含まれる
localStick1 * localStick2 の計算結果は毎フレーム同じ内容になるわけである。$4\times 4$行列の積は、軽いものではない。
Matrix4x4 worldStick3 = localStick1 * localStick2 * localStick3;
Matrix4x4 worldStick2 = localStick1 * localStick2;
Matrix4x4 worldStick1 = localStick1;
上記のような冗長性の問題を解決する最も簡単な方法は、各オブジェクトのローカル行列やワールド行列をインスタンス変数にしてしまうことである。オブジェクトの運動を停止する場合でも、必要なデータをインスタンス変数に保持しておけば、運動を停止したフレーム以降において、そのインスタンス変数を使えばよいだけで、毎フレーム同じ値になるとわかっている計算をする必要がなくなる。
しかし本章では、こういった冗長性の問題をより汎用的なアプローチにより解決する。具体的には、冒頭で述べたように変換処理を専門的に担当するクラスを作成し、オブジェクトの変換はすべてそのクラスを通して行うようにするといった方法である。
Unityや Unreal Enginなどのゲームエンジンには、こういった変換処理専門のクラスが存在する。
Unityでは、Transformクラスというクラスがそれにあたり、オブジェクトを動かす際は、この Transformクラスを経由するのが標準的な方法である。
本章の目的は、オブジェクトを動かすために必要最小限の機能だけを備えた独自のTransformクラスを実装することである。
独自のTransformクラスを実装するのは、5-5節から 5-7節においてであるが、これらの節は読み飛ばして、後に 3Dプログラミングに慣れた段階で取り組むという進め方でも構わない。Transformクラスの構造を理解することは重要なことではあるが、3D空間でオブジェクトを動かすにあたって絶対に必要な知識というわけではない。かといって、特別難しい内容ではないので力試しに目を通すというのもよいであろう。
本章の構成は、5-2節から5-4節でUnityのTransformクラスに関する説明、5-5節から5-8節において独自のTransformクラスの実装及びその使用、5-9節で課題プログラムの作成という順で進められる。先にも述べたが、5-5節から5-8節は飛ばしても構わないが、5-2節から5-4節では Transformクラスの使用に関する基本的な事柄が解説されているので、この節は充分に理解してほしい。