Redpoll's 60
第4章 3D空間におけるオブジェクトの運動

$§$4-4 一体化したオブジェクトの運動 3


本節は読者用の課題である。
4-2節、4-3節における運動の実装では、まず、子オブジェクトの独自の運動を実行し、子オブジェクトを親オブジェクトにアタッチする、次に、親オブジェクトの向き(進行方向)をオイラー角経由で定め、最後に、親オブジェクトに対して3D空間内の指定位置へ平行移動を実行 という手順で進められた(「子オブジェクトの独自の運動」とは、4-2節の飛行機オブジェクトの場合は Propellerの回転であり、4-3節の鳥オブジェクトの場合は両側の Wingのはばたきである)。
つまり、次の3つの手順で実装された。
    (1)  子オブジェクトの独自の運動、及び 親オブジェクトへのアタッチ
    (2)  オイラー角経由による親オブジェクトの向きの設定
    (3)  親オブジェクトの 3D空間内の指定位置への平行移動

本節においても一体化したオブジェクトの運動を実装するが、その実装手順はこの3つの手順に従って行う。そして、各段階の実装は読者自身で行うものとする。それが本節の課題である。

使用するオブジェクトを以下に示す。

  • 図1 Body 初期状態
  • 図2 MainRotor 初期状態
  • 図3 TailRotor 初期状態

図に示されるように、本節において一体化するオブジェクトはヘリコプターである (ここで実装するヘリコプターの運動は、現実のものとは隔たりがある。しかし、本節の目的は一体化したオブジェクトの運動の実装であり、その目的であれば本節の実装で充分である)。

図4 オブジェクトの階層構造
図4にオブジェクトの階層構造を示す。
親子階層は2階層であり、親オブジェクト Bodyに2つの回転オブジェクト、すなわち、MainRotor、TailRotorがアタッチされる。


# Code1
MainRotor、TailRotorを Bodyにアタッチするプログラム(3つのオブジェクトを一体化するプログラム)を以下に示す。

[Beta1A]  (実行結果 図5)
// TailRotor 
Matrix4x4 localTailRotor = TH3DMath.GetTranslation4x4(c_attachPos_TailRotor);

// MainRotor 
Matrix4x4 localMainRotor = TH3DMath.GetTranslation4x4(c_attachPos_MainRotor);

// Body
Matrix4x4 localBody = Matrix4x4.identity; 

// world matrix
Matrix4x4 worldTailRotor = localBody * localTailRotor;
Matrix4x4 worldMainRotor = localBody * localMainRotor;
Matrix4x4 worldBody      = localBody;

TailRotor.SetMatrix(worldTailRotor);
MainRotor.SetMatrix(worldMainRotor);
Body.SetMatrix(worldBody);

図5 Beta1A 実行結果
2行目、5行目で使用されている変数、c_attachPos_MainRotorc_attachPos_TailRotor は、それぞれ MainRotor、TailRotorのアタッチポジションを表す定数である。MainRotorを図2の初期状態の位置から c_attachPos_MainRotor だけ移動させたときの位置が、図5に示される位置(Bodyの上部)である。同様に、TailRotorの場合も図3の初期状態の位置から c_attachPos_TailRotor だけ移動させたときに、図5に示される位置(Bodyの後部)に移る。
Bodyに実行される行列(worldBody)の内容は identity行列であるため、Bodyは初期状態のままである。

次に、MainRotor、TailRotorをそれぞれ初期状態の位置で回転させる。

[Beta1B]  (実行結果 図6)
// MainRotor 
i_degMainRotor -= 12;
Matrix4x4 rotMainRotor = TH3DMath.GetRotation4x4(i_degMainRotor, Vector3.up);
MainRotor.SetMatrix(rotMainRotor);

[Beta1C]  (実行結果 図7)
// TailRotor 
i_degTailRotor -= 20;
Matrix4x4 rotTailRotor = TH3DMath.GetRotation4x4(i_degTailRotor, Vector3.right);
TailRotor.SetMatrix(rotTailRotor);

図6 Beta1B 実行結果 (MainRotorの回転)
図7 Beta1C 実行結果 (TailRotorの回転)

図6は Beta1Bの実行結果で、初期状態の MainRotorに対して y軸周りの回転を実行したものである(初期状態において MainRotorは、y軸がその中心を貫くように配置されている)。Beta1Bの2行目の変数 i_degMainRotor は、MainRotorの回転角度を表すインスタンス変数で、毎フレーム $12$ずつ減少する。これによって MainRotorが毎フレーム $-12$°ずつ回転することになる。
図7は Beta1Cの実行結果で、初期状態の TailRotorに対して x軸周りの回転を実行したものである(初期状態において TailRotorは、x軸がその中心を貫くように配置されており、x軸マイナス方向を向いている)。Beta1Cの2行目の変数 i_degTailRotor は、TailRotorの回転角度を表すインスタンス変数で、毎フレーム $20$ずつ減少する。これによって TailRotorが毎フレーム $-20$°ずつ回転することになる。

図8 Code1 実行結果
以上の準備のもとに最初のプログラムを作成するが、これは読者自身で作成しなければならない。
作成するプログラムは、冒頭で示した3つの手順のうちの「(1) 子オブジェクトの独自の運動、及び 親オブジェクトへのアタッチ」に相当する。
具体的には、2つの回転オブジェクト MainRotor、TailRotorが親オブジェクト Bodyにアタッチされた状態で回転しているプログラムの作成である。つまり、図8の実行結果になるようなプログラムの作成が最初の課題である。

プログラムの記述はソースファイル Sec404.cs 内の Code1に対して行うものとする。Code1は空のメソッドであり、そのまま実行すると、図9のような実行結果が表示される。これは、3つのオブジェクトはデフォルトでは初期状態なので、このように原点付近で重なって表示されてしまうのである。

このプログラムで使用するインスタンス変数、及び 定数を以下に示す (これらの具体的な使用方法は上で述べたとおりである)。
i_degMainRotor
  :  MainRotorの回転角度 (float型  初期値 $0$)
i_degTailRotor
    :  TailRotorの回転角度 (float型  初期値 $0$)
c_attachPos_MainRotor
  :  MainRotorのアタッチポジション (Vector3型)
c_attachPos_TailRotor
    :  TailRotorのアタッチポジション (Vector3型)

このプログラムは、Beta1A、Beta1B、Beta1Cを部分的に組み合わせていけば、簡単に完成する (解答例はソースファイル Sec404_Ans.txt 内の Code1() を参照)。

図9  3つのオブジェクトが初期状態のまま重なっている
図10

なお、余談という形で2つ付け加えておこう。
ここでは詳しい解説は省略するが、第1に ヘリコプターを図10に示されるように(先頭が図の上側、後部が図の下側になるように)見たときに、このプログラムでは、MainRotorの回転は反時計周りになる。この場合には TailRotorは、ヘリコプター後部の左側へ設置する必要がある。第2に このプログラムでは、単位時間あたりの MainRotorの回転数と TailRotorの回転数はほとんど同じであるが、実際のヘリコプターでは、TailRotorの回転数の方が数倍多い値になる (3倍以上の開きがある)。


# Code2
図11
次に2つ目の手順「(2) オイラー角経由による親オブジェクトの向きの設定」を実装する。
オブジェクトを一体化しただけの状態(Beta1A実行後の状態)においては、親オブジェクトである Bodyの向きは図11に示されるように、初期状態の z軸プラス方向を向いている。図11における水色のベクトルは、前節や前々節と同様に Bodyの向きを可視化したものである。

ここでは、前節や前々節と同じくキー操作によって、オイラー角経由で Bodyの向きを設定する。Bodyは親子階層の一番上の親オブジェクトであるから、Bodyの向きを変えるために回転を実行すると、Bodyにアタッチされている MainRotor、TailRotorにも同様の回転が行われるので、3つのオブジェクトが一体になった状態で向きが変化することになる。

キー操作の内容は前節、前々節と同じである。
    H  :  オイラー角のy-角度を $1$減少させる。
    J  :  オイラー角のx-角度を $1$増加させる。
    K  :  オイラー角のx-角度を $1$減少させる。
    L  :  オイラー角のy-角度を $1$増加させる。
Bodyの向きを示す水色のベクトルは、H、Lを押すことで球面における緯線上(横方向)を動き、J、Kを押すことで球面における経線上(縦方向)を動く。

キー操作のコードを以下に示す (以下で使用されている i_eulerXi_eulerY は、オイラー角の x-角度、y-角度を表すインスタンス変数である。z-角度は変化させないので $0$のまま)。
[Beta2]
if (Input.GetKey(KeyCode.H))
{
    i_eulerY -= 1;
}
else if (Input.GetKey(KeyCode.L))
{
    i_eulerY += 1;
}

if (Input.GetKey(KeyCode.J))
{
    i_eulerX += 1;
}
else if (Input.GetKey(KeyCode.K))
{
    i_eulerX -= 1;
}

では、以上の準備のもとに第2のプログラムを作成する。
具体的には、上記のキー操作によってオイラー角経由で、一体化したオブジェクトに対して向きを設定するプログラムの作成である。
プログラムの記述はソースファイル Sec404.cs 内の Code2に対して行うものとする。Code2も空のメソッドであり、そのまま実行すると、図12のような実行結果が表示される (Code1の場合と同様に3つのオブジェクトは初期状態のままである)。

このプログラムで使用するインスタンス変数、及び 定数を以下に示す。
i_eulerX
  :  オイラー角の x-角度 (float型  初期値 $0$)
i_eulerY
  :  オイラー角の y-角度 (float型  初期値 $0$)
c_attachPos_MainRotor
  :  MainRotorのアタッチポジション (Vector3型)
c_attachPos_TailRotor
    :  TailRotorのアタッチポジション (Vector3型)

また、オイラー角による回転行列を取得するためにカスタムライブラリから次のメソッドを使用する。
TH3DMath.GetEulerRotation4x4(float degX, float degY, float degZ)
  :  オイラー角による回転行列を取得する。引数にはx軸周りの回転角度、y軸周りの回転角度、z軸周りの回転角度をこの順で指定する (戻り値は Matrix4x4型)。

このプログラムは、4-2節、4-3節のCode3とほとんど同じであり、それらとの違いは使用するオブジェクトの名前ぐらいであるので作成は容易である。目的とするプログラムは、図13のような実行結果になる (解答例はソースファイル Sec404_Ans.txt 内の Code2() を参照)。

図12  3つのオブジェクトが初期状態のまま重なっている
図13 Code2 実行結果

(文章中の図においては、Bodyの向きを表すために水色のベクトルが表示されているが、実際のプログラムでは表示されることはないので注意)


# Code3
それでは、3つ目の手順「(3) 親オブジェクトの 3D空間内の指定位置への平行移動」を実装する。簡単に言えば、3D空間内をヘリコプターが飛行するプログラムの作成である。
実装方法は前節や前々節と同じく、キー操作によって設定された Bodyの向きに、毎フレーム一定量だけ Bodyを進ませるというものである。Bodyにアタッチされている MainRotorや TailRotorにも同じだけの平行移動が実行されるので、結果的に3つのオブジェクトが一体化して運動することになる。これによって、ヘリコプターの飛行が表現されるのである。

「キー操作によって設定された Bodyの向きに、毎フレーム一定量だけ Bodyを進ませる」ためには、現在の Bodyの向きを計算する必要がある。Code2では Bodyの向きを設定しているが、それは毎フレーム 初期状態の Bodyに対して、オイラー角経由で取得される回転行列を実行して、Bodyの向きを変えているのである。つまり、毎フレーム 初期状態の方向である z軸プラス方向から、オイラー角経由で取得される回転行列を実行して現在の向きに変えているのである。
したがって、現在の Bodyの向きを求めるためには、オイラー角経由で取得される回転行列と z軸プラス方向を表すベクトルの積を計算すればよい (計算結果であるベクトルの値が求める向きである)。
Bodyの向きを取得した後は、その方向に一定量だけ進ませる計算を行うが、具体的には、現時点での Bodyの位置から その方向に一定量だけ進ませる計算をすることになる。現時点での Bodyの位置は、カスタムライブラリの次のメソッドによって取得できる。
GetWorldPosition()
  :  オブジェクトの現在位置を取得する (戻り値は Vector3型)。

Bodyの現在位置であれば、Body.GetWorldPosition() と記述すればよい。位置は Vector3型で返される。
なお、1フレームあたりに進む距離であるが、このプログラムでは $0.05$~$0.15$程度が適している。つまり、「毎フレーム一定量だけ Bodyを進ませる」処理における「一定量」は、$0.1$前後が妥当な値である (ただし、この値はフレームレートが60FPSで設定されている場合での値。毎フレームのオブジェクトの移動量の決定はフレームレートの他に、そのオブジェクトを撮影するカメラの位置にも依存する)。

操作方法は次の通り。
    H、J、K、L  :  Code2と同様にオイラー角による回転でオブジェクトの向きを設定する。
    S  :  飛行の ON/OFF。

Sキーによる飛行の ON/OFF のために、プログラムでは bool型インスタンス変数 i_MOVE が使用できる (初期値はfalse)。i_MOVEtrueであれば、毎フレーム Bodyは一定量だけ進むが、falseのときは毎フレームの移動は行わずに、その場において静止状態になるようにする。静止状態では Bodyの位置に変化がないだけで、向きの変更は可能であるとする。また、MainRotorや TailRotorは常に回転しているものとする。

このプログラムで使用するインスタンス変数、及び 定数を以下に示す。
i_degMainRotor
  :  MainRotorの回転角度 (float型  初期値 $0$)
i_degTailRotor
    :  TailRotorの回転角度 (float型  初期値 $0$)
i_eulerX
  :  オイラー角の x-角度 (float型  初期値 $0$)
i_eulerY
  :  オイラー角の y-角度 (float型  初期値 $0$)
i_MOVE
  :  飛行の ON/OFF のスイッチ (bool型  初期値 false)
c_attachPos_MainRotor
  :  MainRotorのアタッチポジション (Vector3型)
c_attachPos_TailRotor
    :  TailRotorのアタッチポジション (Vector3型)
c_initDir
  :  初期状態における Bodyの向きである z軸プラス方向を表す (同次座標化されているのでVector4型であり、その値は $(0, 0, 1, 1)$ )

図14 Code3 実行結果
プログラムの記述はソースファイル Sec404.cs 内の Code3に対して行うものとする (Code3も空のメソッドであり、そのまま実行すると初期状態の3つのオブジェクトが表示されるだけである)。
上で述べてきたことを実装したプログラムの実行結果は、図14のようになる。
4-2節、4-3節、そして本節では使用するオブジェクトは異なるが、オブジェクトはいずれも3D空間内を飛行するものであり、その飛行の方法についても同じ方法で実装している。したがって、4-2節、4-3節の Code4を理解していれば、このプログラムの作成も難なく行えるであろう (解答例はソースファイル Sec404_Ans.txt 内の Code3() を参照)。


最後に、上で作成したプログラムに機能を1つ追加する。
Code3の実装ではヘリコプターの特徴的な運動である垂直方向の上昇下降ができない。そこで、ヘリコプターが静止状態(i_MOVEfalse)であるときに、垂直方向の上昇下降を行えるように Code3に多少の変更を加える。
静止状態(i_MOVEfalse)のときには、次のキー操作によって上昇下降を行えるようにする。
    I             :  ヘリコプターの上昇。
    Shift + I  :  ヘリコプターの下降。

Shiftが押されているかどうかの判定には、カスタムライブラリの次のメソッドを利用できる。
THUtil.IsShiftDown()
  :  Shiftが押されている状態ならば trueを返し、押されていなければ falseを返す。

例えば、Iキーが押されている状態で Shiftが同時に押されているかどうかの判定をするならば、以下のように記述すればよい。
if(Input.GetKey(KeyCode.I))
{ 
    // Iキーが押されている状態でShiftが押されているか
    if(THUtil.IsShiftDown()){ 
        ...    
    }
}

なお、上昇下降の際の1フレームあたりの移動量であるが、$0.01$程度が妥当な値である (あるいは、Bodyの1フレームあたりに進む量の20%程度の値)

図15
この追加機能は、Code3に対して数行のコードを追加するだけで完成するので独自の空メソッドを用意していない。したがって、プログラムの作成は直接 Code3に変更を加えるという形になる (解答例はソースファイル Sec404_Ans.txt 内の Code3_Vert() を参照)。
追加機能を実装したプログラムを実行すると、図15のような実行結果になる。












© 2020-2024 Redpoll's 60 (All rights reserved)