モデルの回転
當前顯示的頁面不支援所選的顯示語言。
前回は「移動」を行いましたが、向きが変わっていませんでした。もちろん移動する方向によって向きが変わらないとおかしいので、今回はモデルを回転させることにします。Y軸を回転軸としてキーボードの「←→」にそれぞれモデルが回転します。
今回のメインコードファイルを載せます。
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>
モデルのY軸回転(Degree)
</summary>
private float _rotate = 0.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();
return true;
}
<summary>
メインループ処理
</summary>
public void MainLoop()
{
// カメラの設定
this.SettingCamera();
// キー操作で回転パラメータを変化
if (this._keys[(int)Keys.Left])
{
this._rotate += 5.0f;
}
if (this._keys[(int)Keys.Right])
{
this._rotate -= 5.0f;
}
// 描画内容を単色でクリアし、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,
Matrix.RotationY(Geometry.DegreeToRadian(this._rotate)));
// メッシュの描画
this.RenderMesh();
// 文字列の描画
this._font.DrawText(null, "[←→]±Angle", 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, "Angle:" + this._rotate, 0, 36, 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 = 10.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>
モデルのY軸回転(Degree)
</summary>
private float _rotate = 0.0f;
今回は、モデルをY軸を回転軸として回転させるので、保持する値はθの値のみです。通常他にもX軸やZ軸を回転軸にしたり、または任意の回転軸を使ったりするのですが、いきなり複数の回転をやるのも難しいので、今回はY軸限定で行きます。
とりあえず値を「0.0f」で初期化します。単位は「Degree」として扱います。
// キー操作で回転パラメータを変化
if (this._keys[(int)Keys.Left])
{
this._rotate += 5.0f;
}
if (this._keys[(int)Keys.Right])
{
this._rotate -= 5.0f;
}
変化させる値はθのみなので、プラスかマイナスに変化できるように二つのキーを割り当てます。ほとんど今までと処理内容は同じです。
// ライトを有効
this._device.RenderState.Lighting = true;
// 回転のための座標変換
this._device.SetTransform(TransformType.World,
Matrix.RotationY(Geometry.DegreeToRadian(this._rotate)));
// メッシュの描画
this.RenderMesh();
処理内容が「移動」から「回転」に変わっただけです。モデルの位置情報を決定するのが「Device.SetTransform」というのは前回と同じで、第1引数に「TransformType.World」を渡します。
前回と違うのは第2引数で、回転量をマトリックスに変換しなければいけないのですが、そこで使用するのが「Matrix.RotateY」メソッドです。Y軸を回転軸にするので「Y」を使用します。もちろん他に「X」や「Z」もあるので試してみてください。
このメソッドにどのくらい回転させるかを「Radian」で渡します。今回フィールドとして持っているのは「Degree」なので「Geometry.DegreeToRadian」メソッドで「Degree」を「Radian」に変換しています。
その後メッシュを描画すれば完成です。見事にモデルが回転します。
コード的にはこれで終わりですが、計算方法や理屈などもっと知りたいなら、この下を読んでください。
まず「Matrix.RotateY」メソッドですが、渡された値を下のようなマトリックスに変換しています。
なにやら難しそうに見えますが、どうあがいてもこうなるので覚えておいてください。
では前回と同じポリゴンに来てもらいましょう。
ちょうどよく真上から見ているのでこれを使います。
では今回はポリゴンを90度まわしてみましょう。ちなみに Degree の90度は Radian の 0.5πに置き換えられます。まず最初に0番の頂点を計算して見ます。頂点の位置ベクトルは4次元ベクトルに変換します。
残りの頂点は下のような結果になります。
- 0番目 : (1, 0, 1)
- 1番目 : (1, 0, -2)
- 2番目 : (-1, 0, 1)
- 3番目 : (-1, 0, -2)
下が図で表した結果です。
期待通りになりました。