移動、回転、拡大縮小の掛け合わせ
Stránka, ktorú práve prezeráte, nepodporuje vybratý jazyk zobrazenia.
前回までは「移動」「回転」「拡大縮小」をそれぞれ個別に処理していました。しかしそれでは、「移動」と「回転」を同時にやりたいと思ってもできませんでした。今回はそれらを同時に行えるようにマトリックスを掛け合わせて計算させてみます。
今回の操作はゲーム感覚で移動できるようにしています。まず、「←→」でモデルが回転します。そして「↑↓」でモデルが向いている方向に移動し、「ZA」で拡大縮小します。
今回のメインコードファイルを載せます。
MainSample.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;
namespace MDXSample
{
<summary>
メインサンプルクラス
</summary>
public partial class MainSample : IDisposable
{
<summary>
モデルの位置情報
</summary>
private Vector3 _trans = Vector3.Empty;
<summary>
モデルの回転情報(degree)
</summary>
private float _rotate = 0.0f;
<summary>
モデルの拡大縮小情報
</summary>
private Vector3 _scale = new Vector3(1.0f, 1.0f, 1.0f);
<summary>
アプリケーションの初期化
</summary>
<param name="topLevelForm">トップレベルウインドウ</param>
<returns>全ての初期化がOKなら true, ひとつでも失敗したら false を返すようにする</returns>
<remarks>
false を返した場合は、自動的にアプリケーションが終了するようになっている
</remarks>
public bool InitializeApplication(MainForm topLevelForm)
{
// フォームの参照を保持
this._form = topLevelForm;
// 入力イベント作成
this.CreateInputEvent(topLevelForm);
try
{
// Direct3D デバイス作成
this.CreateDevice(topLevelForm);
// フォントの作成
this.CreateFont();
// Xファイルからメッシュ作成
this.LoadXFileMesh("Deruderu.x");
}
catch (DirectXException ex)
{
// 例外発生
MessageBox.Show(ex.ToString(), "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
// XYZライン作成
this.CreateXYZLine();
// ライトを設定
this.SettingLight();
// カメラ空間にトランスフォームされた後で頂点法線の正規化
this._device.RenderState.NormalizeNormals = true;
return true;
}
<summary>
メインループ処理
</summary>
public void MainLoop()
{
// カメラの設定
this.SettingCamera();
// キー操作で移動パラメータを変化
if (this._keys[(int)Keys.Down])
{
// 後へ移動
this._trans -= Vector3.TransformCoordinate(new Vector3(0.0f, 0.0f, -0.3f),
Matrix.RotationY(Geometry.DegreeToRadian(this._rotate)));
}
if (this._keys[(int)Keys.Up])
{
// 前へ移動
this._trans += Vector3.TransformCoordinate(new Vector3(0.0f, 0.0f, -0.3f),
Matrix.RotationY(Geometry.DegreeToRadian(this._rotate)));
}
if (this._keys[(int)Keys.Left])
{
// 左回転
this._rotate -= 5.0f;
}
if (this._keys[(int)Keys.Right])
{
// 右回転
this._rotate += 5.0f;
}
if (this._keys[(int)Keys.Z])
{
// 縮小
this._scale.X /= 1.01f;
this._scale.Y /= 1.01f;
this._scale.Z /= 1.01f;
//this._scale *= 1.0f / 1.01f;
}
if (this._keys[(int)Keys.A])
{
// 拡大
this._scale *= 1.01f;
}
// モデル座標変換用マトリックスを初期化
Matrix modelTransform = Matrix.Identity;
// 最初に拡大縮小
modelTransform *= Matrix.Scaling(this._scale);
// 回転
modelTransform *= Matrix.RotationY(Geometry.DegreeToRadian(this._rotate));
// 最後に移動
modelTransform *= Matrix.Translation(this._trans);
// 描画内容を単色でクリアし、Zバッファもクリア
this._device.Clear(ClearFlags.Target | ClearFlags.ZBuffer, Color.DarkBlue, 1.0f, 0);
// 「BeginScene」と「EndScene」の間に描画内容を記述する
this._device.BeginScene();
// ライトを無効
this._device.RenderState.Lighting = false;
// 原点に配置
this._device.SetTransform(TransformType.World, Matrix.Identity);
// XYZラインを描画
this.RenderXYZLine();
// ライトを有効
this._device.RenderState.Lighting = true;
// 計算したマトリックスで座標変換
this._device.SetTransform(TransformType.World, modelTransform);
// メッシュの描画
this.RenderMesh();
// 文字列の描画
this._font.DrawText(null, "[←→]回転 [↑↓]移動 [ZA]拡大縮小", 0, 0, Color.White);
this._font.DrawText(null, "θ:" + this._lensPosTheta, 0, 12, Color.White);
this._font.DrawText(null, "φ:" + this._lensPosPhi, 0, 24, Color.White);
this._font.DrawText(null,
"移動:"+this._trans.X+","+this._trans.Y+","+this._trans.Z, 0, 36, Color.White);
this._font.DrawText(null, "回転:" + this._rotate, 0, 48, Color.White);
this._font.DrawText(null, "拡大:" + this._scale.X, 0, 60, Color.White);
// 描画はここまで
this._device.EndScene();
// 実際のディスプレイに描画
this._device.Present();
}
<summary>
リソースの破棄をするために呼ばれる
</summary>
public void Dispose()
{
// メッシュの破棄
this.DisposeMesh();
// XYZラインの破棄
this.DisposeXYZLine();
// フォントのリソースを解放
if (this._font != null)
{
this._font.Dispose();
}
// Direct3D デバイスのリソース解放
if (this._device != null)
{
this._device.Dispose();
}
}
}
}
MainSamplePartial.cs ファイルのコードはこちらです。
MainSamplePartial.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;
namespace MDXSample
{
public partial class MainSample
{
<summary>
メインフォーム
</summary>
private MainForm _form = null;
<summary>
Direct3D デバイス
</summary>
private Device _device = null;
<summary>
Direct3D 用フォント
</summary>
private Microsoft.DirectX.Direct3D.Font _font = null;
<summary>
キーのプレス判定
</summary>
private bool[] _keys = new bool[256];
<summary>
1つ前のマウスの位置
</summary>
private Point _oldMousePoint = Point.Empty;
<summary>
カメラレンズの位置(R)
</summary>
private float _lensPosRadius = 20.0f;
<summary>
カメラレンズの位置(θ)
</summary>
private float _lensPosTheta = 270.0f;
<summary>
カメラレンズの位置(φ)
</summary>
private float _lensPosPhi = 45.0f;
<summary>
XYZライン用頂点バッファ
</summary>
private VertexBuffer _xyzLineVertexBuffer = null;
<summary>
メッシュ
</summary>
private Mesh _mesh = null;
<summary>
マテリアル情報配列
</summary>
private ExtendedMaterial[] _materials = null;
<summary>
テクスチャー配列
</summary>
private Texture[] _textures = null;
<summary>
入力イベント作成
</summary>
<param name="topLevelForm">トップレベルウインドウ</param>
private void CreateInputEvent(MainForm topLevelForm)
{
// キーイベント作成
topLevelForm.KeyDown += new KeyEventHandler(this.form_KeyDown);
topLevelForm.KeyUp += new KeyEventHandler(this.form_KeyUp);
// マウス移動イベント
topLevelForm.MouseMove += new MouseEventHandler(this.form_MouseMove);
}
<summary>
キーボードのキーを押した瞬間
</summary>
<param name="sender"></param>
<param name="e"></param>
private void form_KeyDown(object sender, KeyEventArgs e)
{
// 押されたキーコードのフラグを立てる
if ((int)e.KeyCode < this._keys.Length)
{
this._keys[(int)e.KeyCode] = true;
}
}
<summary>
キーボードのキーを放した瞬間
</summary>
<param name="sender"></param>
<param name="e"></param>
private void form_KeyUp(object sender, KeyEventArgs e)
{
// 放したキーコードのフラグを下ろす
if ((int)e.KeyCode < this._keys.Length)
{
this._keys[(int)e.KeyCode] = false;
}
}
<summary>
マウス移動イベント
</summary>
<param name="sender"></param>
<param name="e"></param>
private void form_MouseMove(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
// 回転
this._lensPosTheta -= e.Location.X - this._oldMousePoint.X;
this._lensPosPhi += e.Location.Y - this._oldMousePoint.Y;
// φに関しては制限をつける
if (this._lensPosPhi >= 90.0f)
{
this._lensPosPhi = 89.9999f;
}
else if (this._lensPosPhi <= -90.0f)
{
this._lensPosPhi = -89.9999f;
}
}
// マウスの位置を記憶
this._oldMousePoint = e.Location;
}
<summary>
Direct3D デバイスの作成
</summary>
<param name="topLevelForm">トップレベルウインドウ</param>
private void CreateDevice(MainForm topLevelForm)
{
// PresentParameters。デバイスを作成する際に必須
// どのような環境でデバイスを使用するかを設定する
PresentParameters pp = new PresentParameters();
// ウインドウモードなら true、フルスクリーンモードなら false を指定
pp.Windowed = true;
// スワップ効果。とりあえず「Discard」を指定。
pp.SwapEffect = SwapEffect.Discard;
// 深度ステンシルバッファ。3Dでは前後関係があるので通常 true
pp.EnableAutoDepthStencil = true;
// 自動深度ステンシル サーフェイスのフォーマット。
// 「D16」に対応しているビデオカードは多いが、前後関係の精度があまりよくない。
// できれば「D24S8」を指定したいところ。
pp.AutoDepthStencilFormat = DepthFormat.D16;
try
{
// デバイスの作成
this.CreateDevice(topLevelForm, pp);
}
catch (DirectXException ex)
{
// 例外発生
throw ex;
}
}
<summary>
Direct3D デバイスの作成
</summary>
<param name="topLevelForm">トップレベルウインドウ</param>
<param name="presentationParameters">PresentParameters 構造体</param>
private void CreateDevice(MainForm topLevelForm, PresentParameters presentationParameters)
{
// 実際にデバイスを作成します。
// 常に最高のパフォーマンスで作成を試み、
// 失敗したら下位パフォーマンスで作成するようにしている。
try
{
// ハードウェアによる頂点処理、ラスタライズを行う
// 最高のパフォーマンスで処理を行えます。
// ビデオカードによっては実装できない処理が存在します。
this._device = new Device(0, DeviceType.Hardware, topLevelForm.Handle,
CreateFlags.HardwareVertexProcessing, presentationParameters);
}
catch (DirectXException ex1)
{
// 作成に失敗
Debug.WriteLine(ex1.ToString());
try
{
// ソフトウェアによる頂点処理、ハードウェアによるラスタライズを行う
this._device = new Device(0, DeviceType.Hardware, topLevelForm.Handle,
CreateFlags.SoftwareVertexProcessing, presentationParameters);
}
catch (DirectXException ex2)
{
// 作成に失敗
Debug.WriteLine(ex2.ToString());
try
{
// ソフトウェアによる頂点処理、ラスタライズを行う
// パフォーマンスはとても低いです。
// その代わり、ほとんどの処理を制限なく行えます。
this._device = new Device(0, DeviceType.Reference, topLevelForm.Handle,
CreateFlags.SoftwareVertexProcessing, presentationParameters);
}
catch (DirectXException ex3)
{
// 作成に失敗
// 事実上デバイスは作成できません。
throw ex3;
}
}
}
}
<summary>
フォントの作成
</summary>
private void CreateFont()
{
try
{
// フォントデータの構造体を作成
FontDescription fd = new FontDescription();
// 構造体に必要なデータをセット
fd.Height = 12;
fd.FaceName = "MS ゴシック";
// フォントを作成
this._font = new Microsoft.DirectX.Direct3D.Font(this._device, fd);
}
catch (DirectXException ex)
{
// 例外発生
throw ex;
}
}
<summary>
XYZライン作成
</summary>
private void CreateXYZLine()
{
// 6つ分の頂点を作成
this._xyzLineVertexBuffer = new VertexBuffer(typeof(CustomVertex.PositionColored),
6, this._device, Usage.None, CustomVertex.PositionColored.Format, Pool.Managed);
// 頂点バッファをロックして、位置、色情報を書き込む
using (GraphicsStream data = this._xyzLineVertexBuffer.Lock(0, 0, LockFlags.None))
{
// 今回は各XYZのラインを原点(0.0f, 0.0f, 0.0f)からプラス方向に 10.0f 伸びた線を作成
data.Write(new CustomVertex.PositionColored(0.0f, 0.0f, 0.0f, Color.Red.ToArgb()));
data.Write(new CustomVertex.PositionColored(10.0f, 0.0f, 0.0f, Color.Red.ToArgb()));
data.Write(new CustomVertex.PositionColored(0.0f, 0.0f, 0.0f, Color.Green.ToArgb()));
data.Write(new CustomVertex.PositionColored(0.0f, 10.0f, 0.0f, Color.Green.ToArgb()));
data.Write(new CustomVertex.PositionColored(0.0f, 0.0f, 0.0f, Color.Blue.ToArgb()));
data.Write(new CustomVertex.PositionColored(0.0f, 0.0f, 10.0f, Color.Blue.ToArgb()));
this._xyzLineVertexBuffer.Unlock();
}
}
<summary>
Xファイルからメッシュ関連データを読み込む
</summary>
<param name="xfileName">Xファイル名</param>
private bool LoadXFileMesh(string xfileName)
{
// Xファイルを読み込んでメッシュを作成する
try
{
this._mesh = Mesh.FromFile(xfileName,
MeshFlags.Managed, this._device, out this._materials);
}
catch (DirectXException ex)
{
// メッシュの作成に失敗した場合は例外が飛んでくる
throw ex;
}
// 法線情報がなければ計算して作成
if ((this._mesh.VertexFormat & VertexFormats.Normal) == 0)
{
// 法線情報を加えたメッシュを複製する
Mesh tempMesh = this._mesh.Clone(this._mesh.Options.Value,
this._mesh.VertexFormat | VertexFormats.Normal, this._device);
// 法線を計算
tempMesh.ComputeNormals();
// 古いメッシュを破棄し、置き換える
this._mesh.Dispose();
this._mesh = tempMesh;
}
// テクスチャーがあれば読み込み
if (this._materials.Length >= 1)
{
// テクスチャー用の配列を作成
this._textures = new Texture[this._materials.Length];
// 配列分テクスチャーの読み込みを試みる
for (int i = 0; i < this._materials.Length; i++)
{
// 必ず null で初期化する
this._textures[i] = null;
// テクスチャー名が登録されているか確認
if (this._materials[i].TextureFilename != null &&
this._materials[i].TextureFilename.Length >= 1)
{
try
{
// テクスチャーを読み込む
this._textures[i] = TextureLoader.FromFile(this._device,
this._materials[i].TextureFilename);
}
catch (DirectXException ex)
{
// テクスチャーの作成に失敗した場合は例外が飛んでくる
throw ex;
}
}
}
}
return true;
}
<summary>
ライトの設定
</summary>
private void SettingLight()
{
// 平行光線を使用
this._device.Lights[0].Type = LightType.Directional;
// ライトの方向
this._device.Lights[0].Direction = new Vector3(1.0f, -1.5f, 2.0f);
// 光の色は白
this._device.Lights[0].Diffuse = Color.White;
// 環境光
this._device.Lights[0].Ambient = Color.FromArgb(255, 128, 128, 128);
// 0 番のライトを有効
this._device.Lights[0].Enabled = true;
// 0 番のライトを更新
this._device.Lights[0].Update();
}
<summary>
カメラの設定
</summary>
private void SettingCamera()
{
// レンズの位置を三次元極座標で変換
float radius = this._lensPosRadius;
float theta = Geometry.DegreeToRadian(this._lensPosTheta);
float phi = Geometry.DegreeToRadian(this._lensPosPhi);
Vector3 lensPosition = new Vector3(
(float)(radius * Math.Cos(theta) * Math.Cos(phi)),
(float)(radius * Math.Sin(phi)),
(float)(radius * Math.Sin(theta) * Math.Cos(phi)));
// ビュー変換行列を設定
this._device.Transform.View = Matrix.LookAtLH(
lensPosition, new Vector3(0.0f, 0.0f, 0.0f), new Vector3(0.0f, 1.0f, 0.0f));
// 射影変換を設定
this._device.Transform.Projection = Matrix.PerspectiveFovLH(
Geometry.DegreeToRadian(60.0f),
(float)this._device.Viewport.Width / (float)this._device.Viewport.Height,
1.0f, 100.0f);
}
<summary>
XYZライン描画
</summary>
private void RenderXYZLine()
{
this._device.SetStreamSource(0, this._xyzLineVertexBuffer, 0);
this._device.VertexFormat = CustomVertex.PositionColored.Format;
this._device.DrawPrimitives(PrimitiveType.LineList, 0, 3);
}
<summary>
メッシュの描画
</summary>
private void RenderMesh()
{
// 属性の数だけループさせて描画
for (int i = 0; i < this._materials.Length; i++)
{
// テクスチャーのセット
this._device.SetTexture(0, this._textures[i]);
// マテリアルをセット
this._device.Material = this._materials[i].Material3D;
// 描画
this._mesh.DrawSubset(i);
}
}
<summary>
XYZラインの破棄
</summary>
private void DisposeXYZLine()
{
if (this._xyzLineVertexBuffer != null)
{
this._xyzLineVertexBuffer.Dispose();
}
}
<summary>
メッシュの破棄
</summary>
private void DisposeMesh()
{
// テクスチャーの解放
if (this._textures != null)
{
foreach (Texture i in this._textures)
{
if (i != null)
{
i.Dispose();
}
}
}
// メッシュの解放
if (this._mesh != null)
{
this._mesh.Dispose();
}
}
}
}
<summary>
モデルの位置情報
</summary>
private Vector3 _trans = Vector3.Empty;
<summary>
モデルの回転情報(degree)
</summary>
private float _rotate = 0.0f;
<summary>
モデルの拡大縮小情報
</summary>
private Vector3 _scale = new Vector3(1.0f, 1.0f, 1.0f);
今回持つパラメータは「位置」「回転(Y軸のみ)」「拡大縮小」です。これらの値を組み合わせてモデルの状態を決定します。
// キー操作で移動パラメータを変化
if (this._keys[(int)Keys.Down])
{
// 後へ移動
this._trans -= Vector3.TransformCoordinate(new Vector3(0.0f, 0.0f, -0.3f),
Matrix.RotationY(Geometry.DegreeToRadian(this._rotate)));
}
if (this._keys[(int)Keys.Up])
{
// 前へ移動
this._trans += Vector3.TransformCoordinate(new Vector3(0.0f, 0.0f, -0.3f),
Matrix.RotationY(Geometry.DegreeToRadian(this._rotate)));
}
一度にすべての操作を載せると長くなってしまうので少しずつ説明します。
移動に関しては前の Tips と処理が異なります。一応ここで移動する量を決定しているのですが、何をやっているかというと「移動する方向を回転させて移動する」という処理を行っています。
まず、モデルが一切回転していない場合は向いている方向を「-Z」方向とします。ただこれは今回使用しているモデルに対して決めているのであって、読み込んだXファイルによっては向きが違うので注意してください。通常ゲームなどでは、モデルの方向は統一するように決めるはずです。おそらくプログラムがモデルの向きを調べるのは不可能だと思います。
「-Z」方向を向いているので、前に進む場合は「new Vector3(0.0f, 0.0f, -0.3f)」で -Z に 0.3 ずつ進むように設定しています。 0.3 はスピードなので任意に変えてください。
ではここでモデルを「右回転で90度」回ったとします。もしここで前へ進んだ場合、前と同じく「new Vector3(0.0f, 0.0f, -0.3f)」に進んではおかしくなります(モデルが横歩き状態になります)。そこでモデルが回転したなら、移動すべき方向も回転しよう、という発想が今回の処理です。
ここでやっているのは「回転していない状態での移動方向」に「モデルの向きを変換した行列」を掛け合わせています。式で表すと下のような計算になります。
まず、回転行列を作成します。Y軸を回転軸にして90度右回転の式です。
初期移動ベクトルにこの回転行列を掛け合わせます。
結果移動ベクトルが(-0.3, 0.0, 0.0)になり、向いている方向に進めるようになります。後ろに進む場合は、計算された値を引いているだけです。
if (this._keys[(int)Keys.Left])
{
// 左回転
this._rotate -= 5.0f;
}
if (this._keys[(int)Keys.Right])
{
// 右回転
this._rotate += 5.0f;
}
モデルの回転パラメータを変化させています。前の回転に関しての Tips とやっていることは同じです。
if (this._keys[(int)Keys.Z])
{
// 縮小
this._scale.X /= 1.01f;
this._scale.Y /= 1.01f;
this._scale.Z /= 1.01f;
//this._scale *= 1.0f / 1.01f;
}
if (this._keys[(int)Keys.A])
{
// 拡大
this._scale *= 1.01f;
}
前回の Tips では XYZ 個別に拡大縮小を行っていましたが、今回はまとめて処理しています。
掛け算はベクトルに対して直接掛けることができるのですが、割り算に関しては直接ベクトルに対して割れない仕様になっているので、個別に割り算しています。
もし、まとめて計算したいなら「this.scale *= 1.0f / 1.01f;」と置き換えてください。
// モデル座標変換用マトリックスを初期化
Matrix modelTransform = Matrix.Identity;
// 最初に拡大縮小
modelTransform *= Matrix.Scaling(this._scale);
// 回転
modelTransform *= Matrix.RotationY(Geometry.DegreeToRadian(this._rotate));
// 最後に移動
modelTransform *= Matrix.Translation(this._trans);
さて、キー操作で各パラメータを変化させましたが、これらをマトリックスで合成させます。いままで「Device.SetTransform」メソッドで値を渡すときに直接計算した座標変換値を渡していましたが、今回は別に「Matrix」を宣言しておき、それに対して結果を掛け合わせていきます。
最初に Matrix の変数を宣言しておき、「Matrix.Identity」で初期化します。初期化マトリックスに対してマトリックスに変換した拡大縮小」「回転」「移動」を掛け合わせていきます。足し算ではないので注意してください。
また、マトリックスの掛け算は掛ける順番によって計算結果が違う場合があります(大抵は異なります)。例えば「A×B」と「B×A」がそうです。
これを「移動」と「回転」に置き換えても違いがはっきりとわかります。下の図を見てください。モデルが最終的にいる位置が明らかに違うことがわかります。
上の図が今回使っている順番で、下が別な方法です。
なぜこんな性質があるかというと、すべての座標変換は原点を中心に考えているからです。例えば上の図の下の例だと、「移動した後に回転するんだからその場で回転すればいいじゃん」と思うかもしれませんが、先に移動を行ったためモデルが原点から離れています。ここで回転を掛けているのでモデルが回転するだけでなく、移動した位置も回転してしまうわけです。
そんなわけでマトリックスを掛けるときは順番に十分注意してください。よくわからない人は掛ける順番を「拡大縮小」「回転」「移動」にしたほうが一般的でしょう。もちろん掛け合わせの順番はこれに限らないことを頭に入れておいてください(図の下のパターンはあるポイントをぐるぐる回るという表現に使えます)。
// ライトを有効
this._device.RenderState.Lighting = true;
// 計算したマトリックスで座標変換
this._device.SetTransform(TransformType.World, modelTransform);
// メッシュの描画
this.RenderMesh();
マトリックスを作成したら、あとはモデルの描画前に座標変換をかけるだけです。これでモデルを配置することができます。