MMDモデルを使用したデスクトップユーティリティの開発 その6 視点移動

前: [その5](http://qiita.com/JI1LDG/items/457e76ae15da214c8004)
次: [その7](http://qiita.com/JI1LDG/items/ff6185456138bbd35289)

##今回やること
視点移動を実装し、いろいろな角度・向きからモデルを表示できるようにする。
今回のソース: [GitHub](https://github.com/JI1LDG/ModelViewer/tree/a462720190a215e5433861d28baa25552131141f)

##視点移動
MMDでは、視点水平移動(XY軸移動)・視線の先を対象としたXY軸回転・ズームをマウスの操作によって制御できます。
座標変換行列`View`に移動・回転情報を設定することで実現します。

###RawInput
マウスによる視点移動を行うために、マウスの処理を行わなければなりません。
SlimDXには、RawInputと呼ばれる、デバイス(キーボードやマウスなど)の情報を取得するためのものが付属しているので、それを使用します。
下記のコードでは、マウスとキーボードからの入力を受け取り、処理する関数を定義しています。

```csharp:Core.cs
//using SlimDX.Multimedia;
//using Rwin = SlimDX.RawInput;

protected virtual void MouseInput(object sender, Rwin.MouseInputEventArgs e) { }
protected virtual void KeyInput(object sender, Rwin.KeyboardInputEventArgs e) { }

private void InitializeInputDevice() {
	Rwin.Device.RegisterDevice(UsagePage.Generic, UsageId.Mouse, Rwin.DeviceFlags.None);
	Rwin.Device.MouseInput += MouseInput;
	Rwin.Device.RegisterDevice(UsagePage.Generic, UsageId.Keyboard, Rwin.DeviceFlags.None);
	Rwin.Device.KeyboardInput += KeyInput;
}
```

```csharp:DrawPmdModel.cs
protected override void MouseInput(object sender, Rwin.MouseInputEventArgs e) {
}

protected override void KeyInput(object sender, Rwin.KeyboardInputEventArgs e) {
}
```

RawInputでは、マウスの左/中(ホイール)/右ボタンの押下・押上、ホイールの動き、マウスの移動量を取得できます。
たとえば、左クリックされたときに何か処理をする場合は、以下のように書きます。

```csharp:DrawPmdModel.cs
protected override void MouseInput(object sender, Rwin.MouseInputEventArgs e) {
	switch(e.ButtonFlags) {
	case Rwin.MouseButtonFlags.LeftDown:
		//処理
		break;
	}
}
```

なお、RawInputではマウスのボタンが押されているか(押しっぱなしであるか)や、マウスの位置を取得することはできません。

###水平移動・ズーム
MMDでは、中クリックをしながらマウスを移動させることで、XY軸方向に移動することができます。
これはカメラ(視点)と対象の点(注視点)を一緒に移動させることで実現できます。
さらにMMDでは、ホイールを回転させることでズームイン・ズームアウトができます。
RawInputではホイールの回転量を`e.WheelData`から取得できます。回転してなければ`0`, 奥に向けて回転させると正の値、手前に向けて回転させると負の値です。

```csharp:DrawPmdModel.cs
struct MovingData {
	public int posX;
	public int posY;
	public int posZ;
	public void ResetPosXY() { posX = posY = 0; }
	public void ResetPosZ() { posZ = 0; }
	public void ResetAll() { ResetPosXY(); ResetPosZ(); }
}

bool isMiddleMoving;
MovingData movingNow;

private void UpdateCamera() {
	var world = Matrix.Identity;

	float div = 10;
	var viewEye = new Vector3(0 , 10, -10);
	var viewTarget = new Vector3(0, 10, 0);
	var view = Matrix.Multiply(
		Matrix.LookAtRH(
			viewEye, viewTarget, new Vector3(0, 1, 0)
		),
		Matrix.Translation(movingNow.posX / div, -movingNow.posY / div, movingNow.posZ * 0.2f)
	);
	//省略
}

protected override void MouseInput(object sender, Rwin.MouseInputEventArgs e) {
	switch(e.ButtonFlags) {
		case Rwin.MouseButtonFlags.MiddleDown:
			isMiddleMoving = true;
			break;
		case Rwin.MouseButtonFlags.MiddleUp:
			isMiddleMoving = false;
			break;
	}

	if(isMiddleMoving) {
		movingNow.posX += e.X;
		movingNow.posY += e.Y;
	}
	if(e.WheelDelta > 0) {
		movingNow.posZ++;
	} else if(e.WheelDelta < 0) {
		movingNow.posZ--;
	}
}
```

View行列に移動量を表す行列を掛けることによって、実現させています。
X, Y, Z軸方向に移動する行列は`Matrix.Translation(x, y, z)`で生成できます。

###XY軸回転
MMDでは右クリックしながら動かすことで、回転させることができます。
水平移動のときと同じように回転を表す行列を生成し、掛ければいいのですが、行列には交換法則が適用できないため、掛ける順番に注意が必要です。
今回は、MMDでモデルを選択しているときの回転・移動を基準にしています。
(MMDの表示に近付けるため、視野角を30度に、初期のカメラの位置を変更しております)

```csharp:DrawPmdModel.cs
struct MovingData {
	//省略
	public int rotX;
	public int rotY;
	//省略
	public void ResetRotXY() { rotX = rotY = 0; }
	public void ResetAll() { ResetPosXY(); ResetPosZ(); ResetRotXY(); }
}

bool isRightMoving;

protected override void MouseInput(object sender, Rwin.MouseInputEventArgs e) {
	switch(e.ButtonFlags) {
		//省略
		case Rwin.MouseButtonFlags.RightDown:
			isRightMoving = true;
			break;
		case Rwin.MouseButtonFlags.RightUp:
			isRightMoving = false;
			break;
	}

	//省略
	if(isRightMoving) {
		movingNow.rotX += e.X;
		movingNow.rotY += e.Y;
	}
}

private void UpdateCamera() {
	var world = Matrix.Identity;

	float div = 10;
	var viewEye = new Vector3(0 , 10, -45);
	var viewTarget = new Vector3(0, 10, 0);
	var view = Matrix.Multiply(
		Matrix.Multiply(
			Matrix.RotationX(-movingNow.rotY / 100.0f), 
			Matrix.RotationY(movingNow.rotX / 100.0f)
		), Matrix.Multiply(
			Matrix.LookAtRH(viewEye, viewTarget, new Vector3(0, 1, 0)),
			Matrix.Translation(movingNow.posX / div, -movingNow.posY / div, movingNow.posZ * 0.2f)
		)
	);

var projection = Matrix.PerspectiveFovRH(
	30 * (float)Math.PI / 180, ClientSize.Width / ClientSize.Height, 0.1f, 1000
);
	//省略
}
```

マウスでぐるぐる動かせます。
![cirno_trans_rotate.png](https://qiita-image-store.s3.amazonaws.com/0/135830/0bfe39bd-30f2-cc41-736c-ce3047654fd4.png)

##おまけ
モデルを描画した画像をいくつか掲載していますが、何かおかしいことに気が付いたでしょうか。
スカートの下の部分が黒かったり、羽が透けてなかったりしています。(右がMMDでの表示。羽が透けて腕が見えるのがわかる)
![cirno_no_alpha.png](https://qiita-image-store.s3.amazonaws.com/0/135830/734692f8-318a-c558-daca-8ba4db33abfe.png)![cirno_alphaed_mmd.png](https://qiita-image-store.s3.amazonaws.com/0/135830/101b4475-f6e7-c892-417e-5ba42fbd5c62.png)

アルファブレンドを設定することで解決できます。

```csharp:Core.cs
private void InitializeBlend() {
	var blend = new Dx11.BlendStateDescription() {
		AlphaToCoverageEnable = false, IndependentBlendEnable = false
	};
	for(int i = 0;i < 8; i++) {
		blend.RenderTargets[i] = new Dx11.RenderTargetBlendDescription();
		blend.RenderTargets[i].BlendEnable = true;
		blend.RenderTargets[i].BlendOperation = Dx11.BlendOperation.Add;
		blend.RenderTargets[i].BlendOperationAlpha = Dx11.BlendOperation.Add;
		blend.RenderTargets[i].DestinationBlend = Dx11.BlendOption.InverseSourceAlpha;
		blend.RenderTargets[i].DestinationBlendAlpha = Dx11.BlendOption.Zero;
		blend.RenderTargets[i].RenderTargetWriteMask = Dx11.ColorWriteMaskFlags.All;
		blend.RenderTargets[i].SourceBlend = Dx11.BlendOption.SourceAlpha;
		blend.RenderTargets[i].SourceBlendAlpha = Dx11.BlendOption.One;
	}
	device.ImmediateContext.OutputMerger.BlendFactor = new Color4(1, 1, 1, 1);
	device.ImmediateContext.OutputMerger.BlendSampleMask = 0xffffff;
	device.ImmediateContext.OutputMerger.BlendState = Dx11.BlendState.FromDescription(device, blend);
}
```

![cirno_alphaed_pmv.png](https://qiita-image-store.s3.amazonaws.com/0/135830/a7a6743f-9481-10a6-b58d-6e4eddca14e1.png)

##さいごに
次回は光を扱う予定です。