【Delphi / C++ Builder / Starter】3Dアプリケーションに、視点 カメラを追加してみよう 


#【Delphi / C++ Builder / Starter】3Dアプリケーションに、視点 カメラを追加してみよう 
以前、[「クロスプラットフォーム開発環境 Delphi / C++Builder で 3D表示のアプリを作る」](http://qiita.com/kazaiso/items/12d5a389945cb7cf8823)の題名で、3D表示のアプリの作成をご紹介しましたが、今回、この3Dのアプリに、「カメラ」機能を加え、このカメラからの視点で3D表示を行う方法をご紹介します。

以前の「3D表示のアプリを作る」ではアニメーションを使って3Dの球体をくるくると回しましたが、今度は視点カメラのほうを、３Dの物体を中心に動かして、くるくる回っているように見せます。

余談ですが、Delphi / C++Builder はコンポーネントが豊富にあって、本当にアプリを作るのが楽です。コードを書く量が少ない！

話を戻します。
3Dモデルを作ったアプリの作成は、無料で使えるStarter Editionでも、Windows 32bit向け限定ではありますが、作成できます。
Delphi / C++Builder の Starter Edition は[こちらの記事をご参考](http://qiita.com/kazaiso/items/1c0df8ff902405d1c214)に。

すでにDelphi / C++Builder / RAD Studioをお持ちの方は、そのお持ちの開発環境でお試しください。お持ちでない方はトライアル版使って当記事内容を確認できます。トライアル版導入については、[こちらを記事をご参照](http://qiita.com/kazaiso/items/479fdf1170fa886fe208)ください。

##作成手順のビデオ
作成手順のビデオが公開されています。こちらもどうぞ

* [3Dアプリケーションに、カメラを追加してみよう (Delphi編)](https://youtu.be/Gkh0JRcHfoY)
* [3Dアプリケーションに、カメラを追加してみよう (C++Builder編)](https://youtu.be/M5wp257-eNM)

##3Dアプリの作成と視点カメラ、そしてコーディング
大きく分けて3つのセクションで説明していきます。

* 3Dアプリ作成
* 視点カメラの追加
* 視点(カメラ)を動かすコーディング

###3Dアプリ作成
まずは基本となる3Dの物体表示するアプリを作ってまいります。下記手順をご参照ください。

1. RAD Studioを起動
2. Welcomeページにおいて「マルチデバイスアプリケーションの新規作成」をダブルクリック
3.「空のアプリケーション」を選んで[OK] ボタンをクリックして、空のフォームを作ります
![image.png](https://qiita-image-store.s3.amazonaws.com/0/145847/0eddc34e-0677-ba39-46cc-63c01e86ca67.png)<br><br>
4. 右下のツールパレットから[TViewPort3D]をフォーム上にドラッグアンドドロップします。「ViewPoert3D1」という名称で配置されます<br>
![image.png](https://qiita-image-store.s3.amazonaws.com/0/145847/f1c7e35c-05ed-7ab7-d445-299200b2c4c7.png)<br><br>
5. 「ViewPoert3D1」を選択状態のまま左下のオブジェクトインスペクタにあある「プロパティ」タブの中にある「Align」プロパティを[Client]にに設定し、フォームいっぱいに広げます
<br>![image.png](https://qiita-image-store.s3.amazonaws.com/0/145847/921f98ef-9a45-32eb-ad59-385cc75559d6.png)<br><br>
1. 「Color」のプロパティも変えて色をつけておきます。ここではLightPinkを使用しました
1. ツールパレット内の3D Shapesのグループの中にカテゴライズされているTCube(正六面体)をダブルクリックしてフォーム上に配置します。「Cube1」という名称で配置されます
2. 3Dの物体表面のテクスチャを張り付けるコンポーネントを用意します「TLightMaterialSource」コンポーネントをドラッグアンドロップして配置します<br>
![image.png](https://qiita-image-store.s3.amazonaws.com/0/145847/db50540c-7879-072d-c066-59eadf6be25d.png)
<br>TLightMaterialSourceは張り付けるテクスチャと光源の光の当たり具合をミックスしたビジュアルを与えてくれます<br><br>
1. TLightMaterialSourceの「Texture」プロパティの(Bitmap Empty)と表示されている部分をダブルクリックして、「ビットマップエディタ」を開きます
1. ビットマップエディタの[読み込み]ボタンをクリックして、張り付けるべき画像を選択して[OK]をクリックしてビットマップエディタを閉じます。これでTLightMaterialSourceがはりつけるべき画像を保持している状態になりました
2. 「Cube1」をクリックして選択状態にしてCube1のプロパティのMaterialSourceのドロップダウンボックスからLightMaterialSource1をセットします。これでCube1のテクスチャとしてLightMaterialSource1のもつ画像を使う、という設定ができました
![image.png](https://qiita-image-store.s3.amazonaws.com/0/145847/954d5c65-d41f-024c-51fd-0d36a15bb6b0.png)<br><br>

1. まだこの時点ではCube1上にテクスチャ（画像）は現れません。TLightMaterialSourceは光源とテクスチャをミックスしますので、光源を設定する必要があります。ツールパレットからTLightコンポーネントをViewPort1上に配置してください。「Light1」という名称で配置されます
1. Light1 のPositionプロパティの左側の[+]をクリックして展開し、Zプロパティに [-10]を設定し、Light1の位置を見えやすいところへ変更しておきます
![image.png](https://qiita-image-store.s3.amazonaws.com/0/145847/eeb589f6-f060-73da-d6a5-d94b5d304ca1.png)<BR><BR>

1. 光をななめから充てるように「Light1」の「RotationAngle」プロパティの「X」に[-45]、「Y」に45を設定します
1. 3Dのコンポーネントはつまみを触って角度を変えることができます。Cube1をクリックて表示されるツマミをドラッグして角度をかえてみましょう

これで3D の基本アプリは完成。ここまできたら、一度実行してみましょう。Shift+Ctrl+F9キーを押すか、上部の緑の三角ボタンをクリックして実行します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/145847/c1823bb2-d10d-5d37-b746-e4747fd58ad9.png)
このようにテクスチャが張られた、光源による明暗のが表現された3DのCube1が表示されています。

###視点カメラの追加
では次に、カメラを配置していきます。

1. カメラは「TCamera」コンポーネントです。これを「ViewPort1」上に配置します。「Camera1」として配置されます
2. 「Camera1」の「Position」プロパティの「Z」に[-5]を設定し少しユーザー手前側にひいておきます（デフォルトで-5 が設定されているはず）
3. 「TViewPort3D」はカメラコンポーネントがなくとも３D表示ができるように「デザインカメラ」という設計時の視点と同じ表示をするデフォルトカメラを持っているので、それを使わないようにするため、「ViewPort3D」の「UsingDesginCamera」プロパティを[False]に設定します
4. 同じく「ViewPort3D」の「Camera」プロパティに[Camera1]を設定します（自動で、既に設定されているはずです）。これでCamera1を視点として設定できました。

実行してみると、カメラ視点でより大きく表示されたCubeが表示されます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/145847/dbf8a4c1-fc0a-ad50-c6c2-d26642d9eed7.png)<br><br>

さて、カメラを動かして視点をかえることができますが、Camera1のPositionやRotationAngleプロパティの数値を変更してもターゲットとなるCubeコンポーネントはフレームアウトしてしまいます。そんなときに使えるのがこの TDummyコンポーネントを使ったテクニックです。TDummyは見えないコンポーネント、いわゆるダミーとして使います。Camera1コンポーネントをTDummyの子としてセットし、TDummyダミーをローテーションさせることによりカメラをターゲットの方向に向けたまま衛星軌道のように移動することができます。
これらの手順は下記の通り

1. 「TDummy」をダブルクリックして配置します。「Dummy1」として配置されます。点線で表されている透明なコンポーネントがCube1と同じ位置に配置されているはずです<br>
![image.png](https://qiita-image-store.s3.amazonaws.com/0/145847/655d185c-21b7-6d1a-9cbf-edd5df4249f7.png)<br><br>
1. 左上の「構造」ペインでCamera1をDummy1の上にドラッグアンドドロップして、Dummy1の子として「Camera1」を設定します。<br>
![image.png](https://qiita-image-store.s3.amazonaws.com/0/145847/f6e20a5b-2604-c9ef-9617-ee80608680ea.png)<BR>
この状態で、Dummy1のRotationAngleの値を変えてみると、Camera1も一緒にローテーションすることがおわかりいただけると思います。


###視点(カメラ)を動かすコーディング
ではこのローテションをマウスで操作できるようにイベントハンドラでプログラミングしていきます。

今回は、マウスのドラッグに合わせてDummy1のローテーションプロパティの値を変更し、カメラ(視点)を動かしていきます。
まずはマウスのドラッグを開始した位置を記録しておくためのイベントハンドラを書きます。

1. ViewPort3Dをクリックして選択状態にして、オブジェクトインスペクタ内のタブ「イベント」をクリックしてイベント一覧を表示させます。
2. マウスボタンダウンのイベント OnMouseDownの右側をダブルクリックして、OnMouseDownイベントハンドラを記述するコードエディタへとジャンプし、下記コードを書きます

**Delphi** : Interface部にあるTForm1のPrivate記述も合わせて記載しています

```delphi
type
  TForm1 = class(TForm)
    Viewport3D1: TViewport3D;
    Cube1: TCube;
    LightMaterialSource1: TLightMaterialSource;
    Light1: TLight;
    Camera1: TCamera;
    Dummy1: TDummy;
    procedure Viewport3D1MouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Single);
  private
    { private 宣言 }
    FDown : TPointF;   //マウスダウン初期位置記憶用
  public
    { public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.fmx}

procedure TForm1.Viewport3D1MouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Single);
begin
  FDown := PointF(X, Y);
end;
```

**C++Builder : ヘッダーとcpp部**

```cpp
//--Unit.hに記述
//---------------------------------------------------------------------------
class TForm1 : public TForm
{
__published:	// IDE で管理されるコンポーネント
	TViewport3D *Viewport3D1;
	TCube *Cube1;
	TLight *Light1;
	TLightMaterialSource *LightMaterialSource1;
	TCamera *Camera1;
	TDummy *Dummy1;
	void __fastcall Viewport3D1MouseDown(TObject *Sender, TMouseButton Button, TShiftState Shift,
		  float X, float Y);
private:	// ユーザー宣言
	TPointF FDwon;
public:		// ユーザー宣言
	__fastcall TForm1(TComponent* Owner);
};

//--unit1.cppに記述
//---------------------------------------------------------------------------
void __fastcall TForm1::Viewport3D1MouseDown(TObject *Sender, TMouseButton Button,
		  TShiftState Shift, float X, float Y)
{
	FDwon = PointF(X,Y);
}
```

ここではイベントハンドラの引数として、マウスボタンダウンの位置をXとYで知らせていくれます。つまり、これがマウスのドラッグを始める前のドラッグ開始のポジションXとYになります。これを覚えておき、ドラッグ時に開始点からどの程度動いているかを差し引きで移動距離を算出するのに使います。
そのためTFormのクラスのプライベート変数として XとY座標を記憶しておく TPointF型の FDownフィールドを作っています。
マウスダウン発生時に、そのマウスダウンのXとYを PointFファンクションを使ってTPointF型の値として、FDownに代入しています。


次にマウスがドラッグされたときのイベントハンドラを書きます

1. ViewPort3Dをクリックして選択状態にして、オブジェクトインスペクタ内のタブ「イベント」をクリックしてイベント一覧を表示させます。
1. OnMouseMoveがマウスが動いている時に呼ばれるイベントハンドラです。同様にダブルクリックしてイベントハンドラの記述するコードエディタへジャンプし、下記コードを書きます

**Delphi のコード**

```delphi
procedure TForm1.Viewport3D1MouseMove(Sender: TObject; Shift: TShiftState;
  X, Y: Single);
begin
  if ssLeft in Shift then
  begin
    Dummy1.RotationAngle.X := Dummy1.RotationAngle.X - (Y - FDown.Y);
    Dummy1.RotationAngle.Y := Dummy1.RotationAngle.Y + (X - FDown.X);
    FDown := PointF(X, Y)
  end
end;
```

**C++Builderのコード**

```cpp
//---------------------------------------------------------------------------
void __fastcall TForm1::Viewport3D1MouseMove(TObject *Sender, TShiftState Shift, float X,
          float Y)
{
	if (Shift.Contains(ssLeft)) {
		Dummy1->RotationAngle->X = Dummy1->RotationAngle->X - (Y - FDown.Y);
		Dummy1->RotationAngle->Y = Dummy1->RotationAngle->Y + (X - FDown.X);
		FDown = PointF(X,Y);
	}
}
```

マウスが動いている時、マウスのボタンが押されたまま動いているか、つまり、ドラッグ 動作中であるか判定する為、イベントハンドラの引数 Shiftをチェックします。この引数のTShiftState型はキーの状態を知らせてくれるキーフラグの束であります。

ドラッグ時に押されているべき、マウスの左ボタンは、ssLeftです。これらのキーの値が複合してのShiftの中にはいってきますので、 このssLeftがShiftのメンバに入っているか判定するには in を使って入っているかチェックしています。

入っていれば、Dummy1をローテションしてその子であるカメラを移動させていきます。
Dummy1 のRotationAngleプロパティのXに 入れるのは、現在のXの値に対して、このイベントハンドラで引数として渡されているYの値と、元のマウスの位置の差、つまりマウスが動いた分の値を引いて計算しているいわゆる移動距離分を差し引いた値です。

同様にに Dummy1 のRotationAngle プロパティのY も移動距離分の値だけ変更します。

ローテンションさせた後は、MaouseMoveイベントで渡されている現在のマウスの位置の値、XとYを新たなマウス位置としてFDownに入れておいています。

###実行
これでコーディングができました。実行してみるとマウスダウン＆ドラッグでDummy1のローテーションが行われ、そのDummy1にくっついているCamera1も回って視点が変わり、このCubeの見方をくるくると変える様子が見て取れます。
![cubeMove.gif](https://qiita-image-store.s3.amazonaws.com/0/145847/a65e8542-2ec1-bd26-bcb7-4b86c7403260.gif)


##おわりに
3D表示アプリにカメラコンポーネントを加えて視点を変えてみました。カメラコンポーネントの位置を連続して動かすことで、上記実行例のように物体側がくるくる回っているよう見せることもできますので、応用で様々な使い方ができます。