##今回やること
視点移動を実装し、いろいろな角度・向きからモデルを表示できるようにする。
今回のソース: GitHub
##視点移動
MMDでは、視点水平移動(XY軸移動)・視線の先を対象としたXY軸回転・ズームをマウスの操作によって制御できます。
座標変換行列View
に移動・回転情報を設定することで実現します。
###RawInput
マウスによる視点移動を行うために、マウスの処理を行わなければなりません。
SlimDXには、RawInputと呼ばれる、デバイス(キーボードやマウスなど)の情報を取得するためのものが付属しているので、それを使用します。
下記のコードでは、マウスとキーボードからの入力を受け取り、処理する関数を定義しています。
//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;
}
protected override void MouseInput(object sender, Rwin.MouseInputEventArgs e) {
}
protected override void KeyInput(object sender, Rwin.KeyboardInputEventArgs e) {
}
RawInputでは、マウスの左/中(ホイール)/右ボタンの押下・押上、ホイールの動き、マウスの移動量を取得できます。
たとえば、左クリックされたときに何か処理をする場合は、以下のように書きます。
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
, 奥に向けて回転させると正の値、手前に向けて回転させると負の値です。
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度に、初期のカメラの位置を変更しております)
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
);
//省略
}
##おまけ
モデルを描画した画像をいくつか掲載していますが、何かおかしいことに気が付いたでしょうか。
スカートの下の部分が黒かったり、羽が透けてなかったりしています。(右がMMDでの表示。羽が透けて腕が見えるのがわかる)
アルファブレンドを設定することで解決できます。
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);
}
##さいごに
次回は光を扱う予定です。