ライトで陰を付ける
今まで頂点の色のみで色をつけていましたが、今回は「ライト」を使用してポリゴンに陰をつけてみました。ライトは時間によって移動するようにしたので、それに沿って陰も変化します。
今回のメインコードファイルを載せます。
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 Material _material = new Material();
<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();
}
catch (DirectXException ex)
{
// 例外発生
MessageBox.Show(ex.ToString(), "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
// 箱を作成するための頂点バッファを作成。「位置」と「法線」情報を持たせる
// この2つの情報を持つ構造体は「CustomVertex.PositionNormal」ですでに定義されている。
this._vertexBuffer = new VertexBuffer(typeof(CustomVertex.PositionNormal),
8, this._device, Usage.None, CustomVertex.PositionNormal.Format, Pool.Managed);
// 8点の情報を格納するためのメモリを確保
CustomVertex.PositionNormal[] vertices = new CustomVertex.PositionNormal[8];
// 各頂点を設定
vertices[0] = new CustomVertex.PositionNormal(new Vector3(-2.0f, 2.0f, 2.0f),
Vector3.Normalize(new Vector3(-1.0f, 1.0f, 1.0f)));
vertices[1] = new CustomVertex.PositionNormal(new Vector3(2.0f, 2.0f, 2.0f),
Vector3.Normalize(new Vector3(1.0f, 1.0f, 1.0f)));
vertices[2] = new CustomVertex.PositionNormal(new Vector3(-2.0f, 2.0f, -2.0f),
Vector3.Normalize(new Vector3(-1.0f, 1.0f, -1.0f)));
vertices[3] = new CustomVertex.PositionNormal(new Vector3(2.0f, 2.0f, -2.0f),
Vector3.Normalize(new Vector3(1.0f, 1.0f, -1.0f)));
vertices[4] = new CustomVertex.PositionNormal(new Vector3(-2.0f, -2.0f, 2.0f),
Vector3.Normalize(new Vector3(-1.0f, -1.0f, 1.0f)));
vertices[5] = new CustomVertex.PositionNormal(new Vector3(2.0f, -2.0f, 2.0f),
Vector3.Normalize(new Vector3(1.0f, -1.0f, 1.0f)));
vertices[6] = new CustomVertex.PositionNormal(new Vector3(-2.0f, -2.0f, -2.0f),
Vector3.Normalize(new Vector3(-1.0f, -1.0f, -1.0f)));
vertices[7] = new CustomVertex.PositionNormal(new Vector3(2.0f, -2.0f, -2.0f),
Vector3.Normalize(new Vector3(1.0f, -1.0f, -1.0f)));
// 頂点バッファをロックする
using (GraphicsStream data = this._vertexBuffer.Lock(0, 0, LockFlags.None))
{
// 頂点データを頂点バッファにコピーします
data.Write(vertices);
// 頂点バッファのロックを解除します
this._vertexBuffer.Unlock();
}
// インデックスバッファ作成
// 第2引数の数値は(三角ポリゴンの数)*(ひとつの三角ポリゴンの頂点数)*
// (16 ビットのインデックスサイズ(2byte))
this._indexBuffer = new IndexBuffer(this._device, 12 * 3 * 2, Usage.WriteOnly,
Pool.Managed, true);
// インデックスバッファをロックする
using (GraphicsStream data = this._indexBuffer.Lock(0, 0, LockFlags.None))
{
// インデックスデータをインデックスバッファにコピーします
data.Write(_vertexIndices);
// インデックスバッファのロックを解除します
this._indexBuffer.Unlock();
}
// 物質の色
this._material.Diffuse = Color.FromArgb(255, 255, 255, 255);
// 平行光線を使用
this._device.Lights[0].Type = LightType.Directional;
// 光の色は白
this._device.Lights[0].Diffuse = Color.White;
// 0 番のライトを有効
this._device.Lights[0].Enabled = true;
// 0 番のライトを更新
this._device.Lights[0].Update();
return true;
}
<summary>
メインループ処理
</summary>
public void MainLoop()
{
// カメラの設定
this.SettingCamera();
// ライトを上空でぐるぐる回るようにしている
Matrix mat = Matrix.RotationY(Environment.TickCount / 1000.0f);
// ライトの方向をセット
this._device.Lights[0].Direction =
Vector3.TransformCoordinate(Vector3.Normalize(new Vector3(0.0f, -1.5f, 2.0f)), mat);
this._device.Lights[0].Update();
// 描画内容を単色でクリアし、Zバッファもクリア
this._device.Clear(ClearFlags.Target | ClearFlags.ZBuffer, Color.DarkBlue, 1.0f, 0);
// 「BeginScene」と「EndScene」の間に描画内容を記述する
this._device.BeginScene();
// マテリアルをセット
this._device.Material = this._material;
// 頂点バッファをデバイスのデータストリームにバインド
this._device.SetStreamSource(0, this._vertexBuffer, 0);
// 描画する頂点のフォーマットをセット
this._device.VertexFormat = CustomVertex.PositionNormal.Format;
// インデックスバッファをセット
this._device.Indices = this._indexBuffer;
// レンダリング(描画)
this._device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, 8, 0, 12);
// 文字列の描画
this._font.DrawText(null, "θ:" + this._lensPosTheta, 0, 0, Color.White);
this._font.DrawText(null, "φ:" + this._lensPosPhi, 0, 12, Color.White);
this._font.DrawText(null, "マウス位置:" + this._oldMousePoint, 0, 24, Color.White);
this._font.DrawText(null, "ライトベクトル:" + Environment.NewLine +
this._device.Lights[0].Direction, 0, 36, Color.White);
// 描画はここまで
this._device.EndScene();
// 実際のディスプレイに描画
this._device.Present();
}
<summary>
リソースを破棄するために呼ばれる
</summary>
public void Dispose()
{
// ボックスの破棄
this.DisposeBox();
// フォントのリソースを解放
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 = 300.0f;
<summary>
カメラレンズの位置(φ)
</summary>
private float _lensPosPhi = 30.0f;
<summary>
頂点バッファ
</summary>
private VertexBuffer _vertexBuffer = null;
<summary>
インデックスバッファ
</summary>
private IndexBuffer _indexBuffer = null;
<summary>
インデックスバッファの各頂点番号配列
</summary>
private static Int16[] _vertexIndices = new Int16[] { 2, 0, 1, 1, 3, 2, 4, 0, 2, 2, 6, 4,
5, 1, 0, 0, 4, 5, 7, 3, 1, 1, 5, 7, 6, 2, 3, 3, 7, 6, 4, 6, 7, 7, 5, 4 };
<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>
カメラの設定
</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>
ボックスの破棄
</summary>
private void DisposeBox()
{
// 頂点バッファを解放
if (this._vertexBuffer != null)
{
this._vertexBuffer.Dispose();
}
// インデックスバッファを解放
if (this._indexBuffer != null)
{
this._indexBuffer.Dispose();
}
}
}
}
コードの説明に入る前に補足説明を入れておきます。
今回ライトに関してのサンプルになりますが、ここで使われるキーワードとして「マテリアル」「ライト」「法線」があります。
マテリアル
まず、マテリアルについてですが、マテリアルとは簡単に言い換えれば「物質の色」です。基本的に個々の3Dモデルごとにマテリアルのパラメータを保持しておいて使うことが多いです。
ただし、マテリアルは「ライト」と一緒に使用しなければ意味がありません。逆に言うとライトを使うには「マテリアル」が必要になります。マテリアルの色は頂点の色とは違うので注意してください。
マテリアルの色は頂点の色とは区別して使用されます。マテリアルには複数のパラメータが用意されており、より物質の質感を引き出すことが出来ます。そのパラメータは下の通りです。
Material 構造体 |
|
---|---|
Diffuse | 物質の基本色 |
Ambient | 環境光を受けたときの色(ライトが直接当たらなくても見える) |
Specular | 鏡面反射光(車などの光沢みたいに強く光る) |
SpecularSharpness | 反射の鋭さ(Specular の鋭さ) |
Emissive | 発散光(自分で光る) |
ライトと法線
次にライトと法線についてですが、ライトを使う場合は「法線」は必ず必要になります。これは「陰」を決めるためにパラメータになります。法線は頂点データとして設定することになります。
ライトと物体の位置関係で、どこが明るくなってどこが暗くなるかは簡単にイメージできると思いますが、一応図として載せます。
面がライトのある方向を向いているほうが明るく、逆であるほど暗くなります。ということはこの面の方向を頂点に置き換えても同様なことになります。この向きのことを「法線」と呼びます。
さて、ボックスに設定する法線の設定は2通りあります。下のような感じです。
左と右ではライトを当てたときに違いが出てきます。
左の方法だと面と面の間が角ばって見えるようになります。これは完全に面の法線と一緒の方向を向いているからです。ただし、この方法だと頂点を共有できないというデメリットがありますが、表現方法によってはこのようにしなくてはいけないときもあります。
右の方法だと面と面の間がライトの当て方によって若干まるまって見えるようになります。頂点を共有するのでデータ量が減るというメリットがあります。デメリットは、頂点の法線が面の方向と一緒ではないため、例えば真上からライトをあてても上の面が100%ライトの影響を受けられなくなります。
文で説明してもわかりづらいので、下の図で違いを見てください。
メタセコイアというモデリングソフトで表示したものです
かなり違うことがわかります。
今回は、頂点をわざわざ分割するのが面倒なので、右の方法を使用することにします。
<summary>
マテリアル
</summary>
private Material _material = new Material();
ボックス用のマテリアルを宣言しておきます。マテリアルは常に保持する必要は無く、描画時に毎回作成してセットする方法でもかまいません。用途に応じて使い分けてください。
// 箱を作成するための頂点バッファを作成。「位置」と「法線」情報を持たせる
// この2つの情報を持つ構造体は「CustomVertex.PositionNormal」ですでに定義されている。
this._vertexBuffer = new VertexBuffer(typeof(CustomVertex.PositionNormal),
8, this._device, 0, CustomVertex.PositionNormal.Format, Pool.Managed);
コメントのとおり、「法線」が必要なので「位置」と「法線」を含む「CustomVertex.PositionNormal」構造体を使用しています。今回ライトの影響を見やすくするために「色」データははずしました。色に関してはマテリアルで代用できるのでそちらを使います。
// 8点の情報を格納するためのメモリを確保
CustomVertex.PositionNormal[] vertices = new CustomVertex.PositionNormal[8];
構造体が変わっています。
// 各頂点を設定
vertices[0] = new CustomVertex.PositionNormal(new Vector3(-2.0f, 2.0f, 2.0f),
Vector3.Normalize((new Vector3(-1.0f, 1.0f, 1.0f)));
vertices[1] = new CustomVertex.PositionNormal(new Vector3(2.0f, 2.0f, 2.0f),
Vector3.Normalize((new Vector3(1.0f, 1.0f, 1.0f)));
vertices[2] = new CustomVertex.PositionNormal(new Vector3(-2.0f, 2.0f, -2.0f),
Vector3.Normalize((new Vector3(-1.0f, 1.0f, -1.0f)));
vertices[3] = new CustomVertex.PositionNormal(new Vector3(2.0f, 2.0f, -2.0f),
Vector3.Normalize((new Vector3(1.0f, 1.0f, -1.0f)));
vertices[4] = new CustomVertex.PositionNormal(new Vector3(-2.0f, -2.0f, 2.0f),
Vector3.Normalize((new Vector3(-1.0f, -1.0f, 1.0f)));
vertices[5] = new CustomVertex.PositionNormal(new Vector3(2.0f, -2.0f, 2.0f),
Vector3.Normalize((new Vector3(1.0f, -1.0f, 1.0f)));
vertices[6] = new CustomVertex.PositionNormal(new Vector3(-2.0f, -2.0f, -2.0f),
Vector3.Normalize((new Vector3(-1.0f, -1.0f, -1.0f)));
vertices[7] = new CustomVertex.PositionNormal(new Vector3(2.0f, -2.0f, -2.0f),
Vector3.Normalize((new Vector3(1.0f, -1.0f, -1.0f)));
さて、各頂点の設定です。構造体が変わったので、色ではなく法線を代わりに設定します。法線は上で紹介した「頂点を共有する」の図のように向きを設定してください。向きさえあっていれば各ベクトルの値は適当でかまいません。
指定した法線ベクトルを「Vector3.Normalize」メソッドで正規化します。「向きさえあっていれば各ベクトルの値は適当でいい」という理由はこのためです。
// 物質の色
this._material.Diffuse = Color.FromArgb(255, 255, 255, 255);
ボックス用のマテリアルの設定を行っています。今回は物質の基本色だけ設定しています。
// ライトを設定 /////
// 平行光線を使用
this._device.Lights[0].Type = LightType.Directional;
// 光の色は白
this._device.Lights[0].Diffuse = Color.White;
// 0 番のライトを有効
this._device.Lights[0].Enabled = true;
// 0 番のライトを更新
this._device.Lights[0].Update();
今まで、ライトを無効にしていましたが、今回はライトを使用するのでライトの設定を行います。
ライトといっても実は3種類存在します。「平行光源」「点光源」「スポットライト」の3つです。今回はよく使われる「平行光源」を使用しますが、興味があれば他の二つも試してみてください。ちなみに図で説明すると下のようになります。
Device クラスには「DeviceLights」コレクションがありますが、このうちの「0」のインデックスに設定していきます。基本的には 0 のインデックスを設定すればいいのですが、1 以上のインデックスも設定すれば複数のライトを同時に使用することもできます。ただし、数の上限はビデオカードなどのデバイスに依存するので、使用する前に上限をチェックする必要があります。
ここで設定しているのは、先ほど説明した「ライトの種類」と「色」です。色に関してはマテリアルと似たパラメータがあるのでいろいろ設定してみてください。
本来は「平行光源」を使用しているので「ライトの方向」を設定しなければならないのですが、今回ライトの方向は常に変化するようにしたのでメインループで設定しています。
後は0番のライトを使用するのでフラグを有効にし、最後に「Update」メソッドで更新します。更新しないと設定が反映されないので注意してください。
// ライトを上空でぐるぐる回るようにしている
Matrix mat = Matrix.RotationY(Environment.TickCount / 1000.0f);
// ライトの方向をセット
this._device.Lights[0].Direction =
Vector3.TransformCoordinate(Vector3.Normalize(new Vector3(0.0f, -1.5f, 2.0f)), mat);
this._device.Lights[0].Update();
ここでライトの向きを時間によって変化させています。動きは、ボックスの上でライトがぐるぐる回っているような感じです。図で説明すると下のようになります。
さてコードですが、ベクトルの回転に関してはここでは詳しく説明しません。今回はライトがきちんと使われていればいいので、ライトが回転するのはおまけのようなものです。
とりあえずライトの位置がY軸を中心に時間によって回転しているものだと思ってください。位置の移動や回転などに関しては後の Tips で説明します。
ライトの方向が決まったら、「Light.Direction」にセットして「Light.Update()」で更新します。
// マテリアルをセット
this._device.Material = this._material;
// 頂点バッファをデバイスのデータストリームにバインド
this._device.SetStreamSource(0, this._vertexBuffer, 0);
// 描画する頂点のフォーマットをセット
this._device.VertexFormat = CustomVertex.PositionNormal.Format;
// インデックスバッファをセット
this._device.Indices = this._indexBuffer;
// レンダリング(描画)
this._device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, 8, 0, 12);
ボックスを描画する前にマテリアルを「Device.Material」にセットするようにしています。これにより、これ以降の描画するポリゴンにマテリアルが適用されるようになります。
あとはセットする頂点フォーマットが違うだけで基本的に変わりはありません。