本節ではオブジェクトから弾を発射して的に当てる問題について扱う。
以下の2つのオブジェクトは弾の発射に使われるオブジェクトで図1がCannon、図2がBatteryである (いずれも初期状態)。Cannonは初期状態において x軸プラス方向を向いており、x軸はCannonのいわば中心軸に相当する。Batteryは初期状態においてその中心を y軸が貫いている。
図1 Cannon 初期状態
図2 Battery 初期状態 Cannonは z軸プラス方向が回転軸であり図3に示されるように垂直方向の回転のみを行う。Batteryは y軸プラス方向が回転軸であり図4に示されるように水平方向の回転のみを行う。
図5はこの2つのオブジェクトを一体化したときのもので、Batteryが親オブジェクト、Cannonが子オブジェクトである。CannonをBatteryにアタッチする際には特に移動させる必要はない (アタッチポジションへの移動量は $\boldsymbol{0}$)。そのためこの
2つのオブジェクトが一体化した状態では両者のオブジェクト原点(3-9節)は同じ位置にある。
3D空間内に置かれた的の位置を $P$ とし、Batteryの置かれた位置を $T$ とする (下図)。一体化した状態ではBatteryとCannonのオブジェクト原点の位置は同じであるからこのときのCannonのオブジェクト原点の位置も $T$ である (両者には回転は行われていないので、ここでもCannonは x軸プラス方向を向いている)。
具体的なコードで表せばCannonとBatteryに以下の処理が実行された状態である。
Matrix4x4 localCannon = Matrix4x4.identity;
Matrix4x4 localBattery = GetTranslation4x4(T);
Matrix4x4 worldCannon = localBattry * localCannon;
Matrix4x4 worldBattery = localBattry;
Cannon.SetMatrix(worldCannon);
Battery.SetMatrix(worldBattery);
図6 Battery及びCannonを $T$ まで移動させたときの様子 (回転は行われていないのでCannonは x軸プラス方向を向いている) ここで線分 $TP$ に関して次の2つの角度 $\alpha$、$\beta$ を定める (下図)。$\alpha$ は垂直方向の回転角度であり、$\beta$ は水平方向の回転角度である (図中の $\triangle{TPP'}$ は直角三角形で、$T$ と $P'$ は同じ高さにある。赤い点線は x軸に平行)。
図7 垂直方向の回転角度が α、水平方向の回転角度が β (赤い点線は x軸に平行) CannonからShell(以降の文章中では弾のことを「Shell」と表記する)を発射して、的に当てるようにするためにはCannonのオブジェクト原点と的の位置を結ぶ線分の方向にCannonを向ければよい。今の状況ではCannonのオブジェクト原点の位置は $T$、的の位置は $P$ であるから、下図のようにCannonを向けた状態でShellを発射すればShellは的に向かって進んでいくことになる。
図8 Cannonの中心軸と線分 $TP$ が重なった状態 先程の図6の状態ではCannonは x軸プラス方向を向いているが、これを $P$ の方向に向けるようにするには下図9に示されるようにCannonを垂直方向に $\alpha$ 回転させ、Batteryを水平方向に $\beta$ 回転させればよい。垂直方向、水平方向の回転を行った後でも、この場合にはCannonとBatteryのオブジェクト原点の位置は変化しないので回転前の位置と同じく $T$ に置かれている。
図9 したがって回転後においては線分 $TP$ とCannonの中心軸は図8のように重なった状態になっているのである。
「回転によってオブジェクト原点の位置は変化しない」ことについて1つ注意しておこう。オブジェクト原点を中心に回転を行う場合には回転中心がオブジェクト原点であるため、回転前と回転後ではオブジェクト原点の位置はもちろん変化しない。上の状況では垂直方向と水平方向の2つの回転を行ったが、垂直方向の回転ではその回転軸はCannonのオブジェクト原点を通るので、垂直方向の回転によってCannonのオブジェクト原点の位置は変化しない。同様に水平方向の回転ではその回転軸はBatteryのオブジェクト原点を通るので、水平方向の回転によってBatteryのオブジェクト原点の位置は変化しない。
水平方向の回転によってBatteryのオブジェクト原点の位置が変化しないのはそのためであるが、(親オブジェクトであるBatteryの)水平方向の回転によってCannonのオブジェクト原点の位置が変化しないのは単にCannonとBatteryの
オブジェクト原点の位置が同じ位置にあるからという理由に過ぎない。もし、Cannonのオブジェクト原点の位置がBatteryのオブジェクト原点の位置とは異なる位置にある場合、すなわちCannonをBatteryにアタッチする際に何らかの平行移動が行われていた場合には、親オブジェクトであるBatteryの回転によってCannonのオブジェクト原点の位置は変化する。
例えばCannonをアタッチする際に何らかの平行移動が行われ、両者を一体化した状態が下図10の状態であったとしよう。Batteryのオブジェクト原点を $P_1$、Cannonのオブジェクト原点を $P_2$ とすれば、図に示されるように2つの位置は同じ位置ではない。
この状態からCannonに対し垂直方向の回転を行っても図11に示されるようにCannonのオブジェクト原点の位置は変化しない。しかし、さらにこの後にBatteryに水平方向の回転を行うと子オブジェクトであるCannonに対してもその回転は行われるから、Cannonのオブジェクト原点の位置は図12に示されるようにこの回転によって $P_2$ から ${P_2}'$ に移動するのである (Batteryのオブジェクト原点の位置は $P_1$ のまま)。
親オブジェクトであるBatteryの水平方向の回転によってCannonのオブジェクト原点の位置が変化しないのは、単に先程の状況においては
一体化した状態でも両者のオブジェクト原点が同じ位置にあったからという理由に過ぎないことに注意しておこう (この問題については再度Code4で扱う)。
最後に、実行中のある時点における位置と向きの計算について簡単に述べておこう。
これはいわゆるローカル座標からワールド座標への変換であるが、2D空間の場合においては
2-13節ですでに解説した。3D空間における変換も基本的には2D空間の場合と同じである。
例えばオブジェクトの初期状態における点 $P_1$ の座標が $(x, y, z)$、$\boldsymbol{\mathsf{v}} = \overrightarrow{P_1P_2}$ とするときベクトル $\boldsymbol{\mathsf{v}}$ の成分が $(a, b, c)$ であったとしよう (下図13)。
図13
図14 このオブジェクトがある時点において図14の状態になり、このときオブジェクトに実行されている変換行列が $M$ であったとする。
その場合図14における点 $P_1$ の座標を $(x',\ y',\ z')$ とすれば\[\begin{pmatrix}x' \\y' \\z' \\1 \end{pmatrix}=M\begin{pmatrix}x \\y \\z \\1 \end{pmatrix}\]であり、図14におけるベクトル $\boldsymbol{\mathsf{v}}$ の成分を $(a',\ b',\ c')$ とすれば\[\begin{pmatrix}a' \\b' \\c' \\0 \end{pmatrix}=M\begin{pmatrix}a \\b \\c \\0 \end{pmatrix}\]である。
つまり、ある時点における位置を求める場合には行列との積において w座標を $1$ にし、ある時点における向きを求める場合には行列との積において w座標を $0$ にするのである (詳しくは
4-12節、あるいは
4-13節参照)。
以上をふまえてShellを的に当てるプログラムを作成するが、本節のプログラムはすべて読者用の課題である。
# Code1
最初のプログラムは上の状況をそのまま使用する。
3D空間内に置かれた的の位置を $P$、Battery(及びCannon)の位置を $T$ とし、線分 $TP$ の垂直方向の回転角度を $\alpha$、水平方向の回転角度を $\beta$ としたときのこの2つの回転角度を求め、Cannon及びBatteryを回転させて目的の位置にShellを発射することがここでの課題である (2つの角度 $\alpha$、$\beta$ の求め方については
3-4節 Code4参照)。
$T$ に移動した状態では回転は行われていないのでCannonは初期状態と同じく x軸プラス方向を向いている (下図)。
図15 Battery及びCannonのオブジェクト原点の位置はともに $T$ (赤い点線は x軸に平行) ここで使われるShellは図16に示される球体オブジェクトであり、初期状態ではその中心が原点に置かれている。また、Cannonの初期状態におけるShell発射開始位置を $S_0$ とすればその位置は図17に示されるように $(5, 0, 0)$ である (発射開始位置については
2-11節参照)。
図16 Shell 初期状態
図17 Cannonの初期状態における発射開始位置 Shellの発射は S キーが押されてから発射されるものとする。その際には S キーが押されてからすぐに発射するのではなく、120フレームかけてCannonとBatteryを回転させ、キーが押されてから120フレーム後に発射されるものとする (下図)。
図18 Code1 実行結果 プログラムに用意されている定数及びインスタンス変数は以下のとおり。
P
: 的の位置 (
Vector3型)。
T
: Battery(及びCannon)の位置 (
Vector3型)。
S0
: Cannonの初期状態におけるShellの発射開始位置 (図17の $S_0$ ;
Vector3型)。
i_alpha
: Cannonの回転角度(垂直方向の回転角度)に用いる
float型インスタンス変数。
i_beta
: Batteryの回転角度(水平方向の回転角度)に用いる
float型インスタンス変数。
i_shootDirection
: Shellの発射方向を表す
Vector3型インスタンス変数 (単位ベクトルとして使用する)。
i_motionCounter
: フレームカウント用の
int型インスタンス変数。
i_START
: S キーが押されたことを表す
bool型インスタンス変数。
最初の段階ではプログラムは以下のように記述されている。
[Code1] (実行結果 図18)
if (Input.GetKeyDown(KeyCode.R))
{
Reset();
}
if (!i_START)
{
if (Input.GetKeyDown(KeyCode.S))
{
i_motionCounter = 0;
i_START = true;
}
else { return; }
}
i_motionCounter++;
if (i_motionCounter <= 120)
{
if (i_motionCounter == 120)
{
}
}
else
{
Vector3 pos = Shell.GetWorldPosition();
pos += 0.2f * i_shootDirection;
Shell.SetWorldPosition(pos);
}
i_START の初期値は
falseなので S キーが押されると8行目の
ifブロックに入るが、この
ifブロックにおいて垂直方向の回転角度 $\alpha$、水平方向の回転角度 $\beta$ 及びShellの発射方向を計算する。Shellの発射方向をセットするためにインスタンス変数
i_shootDirection が用意されているが、このインスタンス変数は
単位ベクトルとして使用することが前提である。
S キーが押されると17行目以降に処理が進む。19行目の
ifブロックは120フレームかけてCannon及びBatteryを回転させるためのものである (
i_motionCounter の値が $120$ になったときにCannonが的の位置 $P$ を向いた状態になるようにする)。120フレーム目において23行目の
ifブロックに入るが、ここでShellを発射開始位置に移動させる。
それ以降は28行目の
elseブロックに入り続けるが、この部分の処理はShellを
i_shootDirection の方向に毎フレーム少しずつ移動させるだけであり、この部分は
変更する必要はない。
また本節のプログラムでは、どのような状況においても R キーが押されると開始時点の状態に戻る(1行目)。
$P$ は青い円の中心 (注 : 本節で使われる的は右図に示されるように円盤であるが、「的の位置 $P$」はその円盤の中心(図の青い円の中心)のことである。Shellを発射すると発射方向を表す直線が表示されるが、その直線が $P$ を貫通していればShellは $P$ を通過する。そのため的にShellが当たったかどうかの判定は、発射方向を表す直線が $P$ を通過しているかどうかで判定している。ただし実際には直線が青い円を貫通していれば当たったとみなされるので、判定自体は厳密なものではない。直線が $P$ を貫通していないと判定された場合にはShellは円盤をすり抜ける)
# Code2
続いてオブジェクトを1つ追加する。
図19はオブジェクト Block の初期状態である。Blockは直方体でその上面はちょうどXZ平面上にあり、上面の中心は原点に一致する (つまり上面の中心がBlockのオブジェクト原点)。また上面に示される矢印は初期状態では z軸プラス方向を向いている。
Blockは上記のBatteryの親オブジェクトであり、階層構造の一番上に位置する (図20)。
図19 Block 初期状態 (上面の矢印は z軸プラス方向を向いている)
図20 オブジェクトの階層構造 ここでBlockを下図に示される位置 $A_1$ に移動させたとする。下図では3つのオブジェクトが一体化した状態であり、今回もBatteryとCannonのオブジェクト原点は同じ位置にある。しかし、BatteryをBlockにアタッチする際には平行移動が行われているためBlockのオブジェクト原点の位置はBatteryとCannonのオブジェクト原点の位置とは異なる。
下図に示される $A_1$ はBlockのオブジェクト原点の位置であるが、Battery及びCannonのオブジェクト原点の位置は $A_1$ から離れた位置にある。また図中の $\boldsymbol{\mathsf{w}}$ はBlock上面の矢印の向きを表すが、以下では $\boldsymbol{\mathsf{w}}$ を
単位ベクトルとする。
図21 $A_1$ はBlockのオブジェクト原点の位置 (Battery及びCannonのオブジェクト原点の位置は $A_1$ から離れた位置にある) 下図22はこの状況を真上から見下ろしたときのものである。図中の青い点線は z軸に平行であり、$\boldsymbol{\mathsf{w}}$ と $20^\circ$ の角をなす。具体的には、このときのBlockには y軸周りに $-20^\circ$ の回転が行われている。
図22 Blockには y軸周りに $-20^\circ$ の回転が行われている (青い点線は z軸に平行) 今回のプログラムでは開始時点において以下の行列が各オブジェクトに実行されている (2行目の
c_attachPos_Battery はBatteryをBlockにアタッチする際の移動量である)。
Matrix4x4 localCannon = Matrix4x4.identity;
Matrix4x4 localBattery = GetTranslation4x4(c_attachPos_Battery);
Matrix4x4 localBlock = GetTranslation4x4(A1) * GetRotation4x4(-20, Vector3.up);
Matrix4x4 worldCannon = localBlock * localBattery * localCannon;
Matrix4x4 worldBattery = localBlock * localBattery;
Matrix4x4 worldBlock = localBlock;
Cannon.SetMatrix(worldCannon);
Battery.SetMatrix(worldBattery);
Block.SetMatrix(worldBlock);
この状況からBlockを $\boldsymbol{\mathsf{w}}$ の方向に毎フレーム $0.05$ ずつ移動させるとき、Blockのオブジェクト原点は下図に示されるように $A_1$ をスタート地点として $A_2$、$A_3$ と進んでいく。$A_2$ から $A_3$ まではちょうど $120$ フレームであり、したがって $A_3$ の位置は $A_2$ から $\boldsymbol{\mathsf{w}}$ の方向に $0.05 \times 120$ だけ離れているわけである。
図23 ここでの課題は上記のように移動するBlock上においてCannonとBatteryを回転させ、Shellを的に当てることである。ただしCannonとBatteryの回転は、Blockのオブジェクト原点が $A_2$ から $A_3$ を移動する $120$ フレームの間に行われるようにし、$A_3$ に到達した時点でShellが発射されるようにするものとする。
以下はプログラムに用意されている定数及びインスタンス変数であるが、$c\_attachPos\_Battery$ と $\boldsymbol{\mathsf{w}}$ 以外はCode1と役割は同じである。
P, S0, i_alpha, i_beta, i_shootDirection, i_motionCounter, i_START
w
: Blockの進行方向表す単位ベクトル (
Vector3型)
c_attachPos_Battery
: BatteryをBlockにアタッチする際の移動量 (
Vector3型)
プログラムは最初の段階では次のように記述されている。
[Code2] (実行結果 図24)
if (Input.GetKeyDown(KeyCode.R))
{
Reset();
}
if (!i_START)
{
if (Input.GetKeyDown(KeyCode.S))
{
i_START = true;
}
else { return; }
}
int phase = GetCurrentPhase();
if (phase == PHASE_0)
{
i_motionCounter = 0;
}
else if (phase == PHASE_1)
{
i_motionCounter++;
Matrix4x4 localBlock = GetBlockMatrix();
// Matrix4x4 worldCannon = ... ;
// Matrix4x4 worldBattery = ... ;
// Cannon.SetMatrix(worldCannon);
// Battery.SetMatrix(worldBattery);
if (i_motionCounter == 120)
{
}
}
else if (phase == PHASE_2)
{
Vector3 pos = Shell.GetWorldPosition();
pos += 0.2f * i_shootDirection;
Shell.SetWorldPosition(pos);
}
開始時点では上図に示されるようにBlock(のオブジェクト原点)は $A_1$ に置かれ、Blockに対しては y軸周りの $-20^\circ$ の回転が行われている。S キーが押されるとBlockが $\boldsymbol{\mathsf{w}}$ の方向に毎フレーム $0.05$ ずつ移動を始める。
15行目の
GetCurrentPhase() は進行状況を
int型定数で返すメソッドであり、Blockが $A_2$ に到達した際に
GetCurrentPhase() は定数
PHASE_0 を返すので、そのフレームにおいては17行目の
ifブロックに処理が移る。17行目の
ifブロックに入るのはそのフレームにおいてのみであり、この
ifブロックで垂直方向の回転角度 $\alpha$、水平方向の回転角度 $\beta$ 及びShellの発射方向を計算する。
$A_2$ に到達したフレームの次のフレームからは
GetCurrentPhase() は
PHASE_1 を返すが、これは $A_3$ に到達するまでの $120$ フレームの間続けて
PHASE_1 が返される。したがって22行目の
else/if ブロックには $A_3$ に到達するまでの $120$ フレームの間入り続けるのでこのブロックにおいてBatteryとCannonの回転を行い、$A_3$ に到達する最後のフレームでShellを発射開始位置に移動させる (35行目の
ifブロック)。
なおこの
ifブロックでは29~33行目に示されるように、BatteryとCannonの変換行列だけを求めてBatteryとCannonだけに実行すればよい。Blockには変換行列はすでに実行されており、それは27行目の
GetBlockMatrix() で毎フレーム取得されるので、この行列を用いてBatteryとCannonの変換行列を計算することができる。
それ以降($A_3$ に到達した次のフレーム以降)は
GetCurrentPhase() は
PHASE_2 を返すので、Shell発射後は40行目の
else/ifブロックに入り続ける。このブロックはCode1でもそうだったが特に処理を変更する必要はない。Shellの発射開始位置と
i_shootDirection の値が正しければ一定時間経過後にShellは的に当たる。
繰り返しになるが、このプログラムではBlockに対する処理(
SetMatrix(..)など)を記述する必要はない。また
PHASE_2 以降のBatteryとCannonの運動についても裏側で自動的に計算されるので、
PHASE_2 以降の処理は記述する必要はない。処理の記述は
PHASE_0 と
PHASE_1 の
ifブロック(17行目、22行目)だけでよい。
図24 Code2 実行結果 # Code3
(注意 : この課題は簡単ではないので3D空間内の状況をイメージすることに慣れてから、少なくともこの後に扱うローカル座標系を学習してから挑戦するようにした方がよい。ローカル座標系を知らなくてもできないことはないが回り道をする方が無難である)
今回のプログラムは設定自体はCode2と同じであるが、開始時点におけるBlockの位置と回転状態のみが異なる (下図)。今回は開始時点でBlockは $B_1$ に置かれ、適当な回転が実行されている。Code2では y軸周りの回転だけであったのでBlockの上面や底面はXZ平面に平行であったが、下図のBlockに行われている回転はそのような単純な回転ではないので、Blockの上面や底面はXZ平面に平行ではない。
これはBlockの進行方向 $\boldsymbol{\mathsf{w}}$ についても同じであり、Code2では $\boldsymbol{\mathsf{w}}$ はXZ平面に平行であったのでBlockがどれだけ進んでもBlockは上下方向(y軸方向)に移動することはなかったが、今回の $\boldsymbol{\mathsf{w}}$ はXZ平面に平行ではないのでBlockが進んでいく間にBlockはXZ平面に近づいていく。
図25 上図はプログラム開始時点における状態であるが、各オブジェクトには以下の行列が実行されている (3行目の
mtxR はBlockを上図の状態にする回転行列)。
Matrix4x4 localCannon = Matrix4x4.identity;
Matrix4x4 localBattery = GetTranslation4x4(c_attachPos_Battery);
Matrix4x4 localBlock = GetTranslation4x4(B1) * mtxR;
Matrix4x4 worldCannon = localBlock * localBattery * localCannon;
Matrix4x4 worldBattery = localBlock * localBattery;
Matrix4x4 worldBlock = localBlock;
Cannon.SetMatrix(worldCannon);
Battery.SetMatrix(worldBattery);
Block.SetMatrix(worldBlock);
Blockが上図の状態から $\boldsymbol{\mathsf{w}}$ の方向に毎フレーム $0.05$ ずつ進んでいくとする (このプログラムでも $\boldsymbol{\mathsf{w}}$ は単位ベクトルである)。そのときBlockのオブジェクト原点は $B_1$ をスタート地点として $B_2$、$B_3$ を通過するが、$B_2$ から $B_3$ の移動にかかる時間はここでも $120$ フレームである。したがって $B_3$ は $B_2$ から $\boldsymbol{\mathsf{w}}$ の方向に $0.05 \times 120$ 離れた位置にあるわけである。
今回の課題も移動するBlock上においてCannonとBatteryを回転させ、Shellを的に当てることである。その際 Code2と同様にCannonとBatteryの回転は、Blockのオブジェクト原点が $B_2$ から $B_3$ を移動する $120$ フレームの間に行われるようにし、$B_3$ に到達した時点でShellが発射されるようにするものとする。
プログラムに用意されている定数及びインスタンス変数はCode2と同じである。
P, S0, i_alpha, i_beta, i_shootDirection, c_attachPos_Battery,
w, i_motionCounter, i_START
プログラムは最初の段階では次のように記述されている。
[Code3] (実行結果 図26)
if (Input.GetKeyDown(KeyCode.R))
{
Reset();
}
if (!i_START)
{
if (Input.GetKeyDown(KeyCode.S))
{
i_START = true;
}
else { return; }
}
int phase = GetCurrentPhase();
if (phase == PHASE_0)
{
i_motionCounter = 0;
}
else if (phase == PHASE_1)
{
i_motionCounter++;
Matrix4x4 localBlock = GetBlockMatrix();
// Matrix4x4 worldCannon = ... ;
// Matrix4x4 worldBattery = ... ;
// Cannon.SetMatrix(worldCannon);
// Battery.SetMatrix(worldBattery);
if (i_motionCounter == 120)
{
}
}
else if (phase == PHASE_2)
{
Vector3 pos = Shell.GetWorldPosition();
pos += 0.2f * i_shootDirection;
Shell.SetWorldPosition(pos);
}
今回のプログラムは開始時点におけるBlockの位置と回転状態が異なるだけでそれ以外は実質的にはCode2と同じである。そのため上記の
PHASE_0、
PHASE_1、
PHASE_2 の各ブロックの役割も同じである ($A_1$、$A_2$、$A_3$ が $B_1$、$B_2$、$B_3$ に変わったに過ぎない)。
すなわち、Blockのオブジェクト原点が $B_2$ に到達したフレームにおいては
PHASE_0 の
ifブロックに入り(17行目)、$B_2$ に到達したフレームの次のフレームから $B_3$ に到達するまでの $120$ フレームは
PHASE_1 の
else/ifブロックに入り(22行目)、$B_3$ に到達したフレームの次のフレーム以降は
PHASE_2 の
else/ifブロックに入り続ける(40行目)。
したがって記述する処理はCode2と同じく、
PHASE_0 のブロックにおいては垂直方向の回転角度 $\alpha$、水平方向の回転角度 $\beta$ 及びShellの発射方向を計算を、
PHASE_1 のブロックにおいてはBatteryとCannonの運動(及びShellの発射開始位置)だけを記述すればよい。
図26 Code3 実行結果 # Code4
最後のプログラムではBatteryの形状を変更する。下図27が今回使用するBatteryであるが、今までのBatteryと異なる点はCannonをアタッチする際に平行移動が必要になるという点である。図28は2つのオブジェクトを一体化した状態であり、図中の $T$、$C$ はそれぞれBatteryのオブジェクト原点の位置、Cannonのオブジェクト原点の位置を表している。
Batteryは初期状態であるため $T$ の位置は座標系の原点であるが、Cannonのオブジェクト原点の位置 $C$ は図から明らかなように座標系の原点から離れた位置にある。今回は
一体化した状態においてCannonのオブジェクト原点とBatteryのオブジェクト原点の位置は同じではないのである。
図27 Battery 初期状態
図28 一体化した状態 ($T$ はBatteryのオブジェクト原点、$C$ はCannonのオブジェクト原点) 下図は一体化した状態のCannonとBatteryを適当な位置に移動させたときのものである。
図29 この状況を真上から見下ろしたときの図を以下に示す。図中の $T$ はBatteryのオブジェクト原点、$C$ はCannonのオブジェクト原点の位置を表している ($P$ は今までと同様に的の位置)。2つのオブジェクトには回転は行われていないので Cannonは初期状態と同じく x軸プラス方向を向いている ($T$ から引かれている点線は x軸に平行)。
図30 真上から見下ろしたとき ($T$ はBatteryのオブジェクト原点、$C$ はCannonのオブジェクト原点。赤い点線は x軸に平行) ここでの課題は上図29の状態から、Batteryを水平方向にCannonを垂直方向に回転させてShellを発射し、的に当てることである。今までと同様に、的の方向にCannonを向けるための回転は $120$ フレームかけて行うようにする。
プログラム開始時点(図29)においては2つのオブジェクトには以下の行列が実行されている。
Matrix4x4 localCannon = GetTranslation4x4(c_attachPos_Cannon);
Matrix4x4 localBattery = GetTranslation4x4(T);
Matrix4x4 worldCannon = localBattery * localCannon;
Matrix4x4 worldBattery = localBattery;
Cannon.SetMatrix(worldCannon);
Battery.SetMatrix(worldBattery);
1行目の
c_attachPos_Cannon はCannonをBatteryにアタッチする際の移動量を表す定数である (
Vector3型)。図29(あるいは図30)の状態においてBatteryのオブジェクト原点の位置は $T$ であるが、このときのCannonのオブジェクト原点の位置 $C$ は
Vector3 C = T + c_attachPos_Cannon;
として求められる。
プログラムに用意されている定数及びインスタンス変数はCode1と同じであり、上記の
c_attachPos_Cannon が追加されただけである。
P, T, S0, i_alpha, i_beta, i_shootDirection, c_attachPos_Cannon, i_motionCounter, i_START
最初の段階ではプログラムは以下のように記述されている。
[Code4] (実行結果 図31)
if (Input.GetKeyDown(KeyCode.R))
{
Reset();
}
if (!i_START)
{
if (Input.GetKeyDown(KeyCode.S))
{
i_motionCounter = 0;
i_START = true;
}
else { return; }
}
i_motionCounter++;
if (i_motionCounter <= 120)
{
if (i_motionCounter == 120)
{
}
}
else
{
Vector3 pos = Shell.GetWorldPosition();
pos += 0.2f * i_shootDirection;
Shell.SetWorldPosition(pos);
}
このプログラムは垂直方向、水平方向の回転角度(及びShellの発射方向)の計算内容が異なるだけであり、それ以外についてはCode1と同じである (S キーが押された際に入る8行目の
ifブロックの内容以外はCode1と同じになる)。
図31 Code4 実行結果 本節の課題はいずれもShellを的に当てるというものであるが、実際に問題としているのは角度の計算である。計算された角度が正しければShellの発射方向も正しいものとなり、当然の帰結としてその方向に進んでいくShellは的に当たることになる。
コンピューターグラフィックスではしばしば角度を求める状況(内積や外積の計算も含む)が発生するが、その際に重要なのはその角度を測る基準である。つまり何を基準にして角度を測るのかということが、角度の計算において正しい値と間違った値の分かれ目になる。本節で扱った4つの状況は角度を求める問題としてはそこまで複雑な状況ではないが、角度を測る基準を間違いやすい問題ではある。ここで求める角度が単独のオブジェクトの回転角度ではなく、階層構造に含まれるオブジェクトの回転角度であるという点には注意しなければいけない (それが問題を面倒にしているのである)。
(本節の課題の解答例については Sec404_Ans.txt を参照 ; Sec404_Ans.txtはダウンロードコンテンツ内の「txt_ans」フォルダに含まれている)。