ライトで陰を付ける
今まで頂点の色のみで色をつけていましたが、今回は「ライト」を使用してポリゴンに陰をつけてみました。ライトは時間によって移動するようにしたので、それに沿って陰も変化します。

下のリンクから今回のプロジェクトをダウンロードできます。
今回のメインコードファイルを載せます。重要なコードを赤色で表示させています。
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></returns>
/// <remarks>
///
/// </remarks>
public bool InitializeApplication(MainForm topLevelForm)
{
this._form = topLevelForm;
this.CreateInputEvent(topLevelForm);
try
{
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();
}
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();
this._device.Clear(ClearFlags.Target | ClearFlags.ZBuffer, Color.DarkBlue, 1.0f, 0);
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();
}
if (this._device != null)
{
this._device.Dispose();
}
}
}
}
|
では、赤文字の部分を説明していきます。MainSamplePartial.cs ファイルのコードはこちらです。
コードの説明に入る前に補足説明を入れておきます。
今回ライトに関してのサンプルになりますが、ここで使われるキーワードとして「マテリアル」「ライト」「法線」があります。
まず、マテリアルについてですが、マテリアルとは簡単に言い換えれば「物質の色」です。基本的に個々の3Dモデルごとにマテリアルのパラメータを保持しておいて使うことが多いです。
ただし、マテリアルは「ライト」と一緒に使用しなければ意味がありません。逆に言うとライトを使うには「マテリアル」が必要になります。マテリアルの色は頂点の色とは違うので注意してください。
マテリアルの色は頂点の色とは区別して使用されます。マテリアルには複数のパラメータが用意されており、より物質の質感を引き出すことが出来ます。そのパラメータは下の通りです。
|
Material 構造体
|
| Diffuse |
物質の基本色 |
| Ambient |
環境光を受けたときの色(ライトが直接当たらなくても見える) |
| Specular |
鏡面反射光(車などの光沢みたいに強く光る) |
| SpecularSharpness |
反射の鋭さ(Specular の鋭さ) |
| Emissive |
発散光(自分で光る) |
次にライトと法線についてですが、ライトを使う場合は「法線」は必ず必要になります。これは「陰」を決めるためにパラメータになります。法線は頂点データとして設定することになります。
ライトと物体の位置関係で、どこが明るくなってどこが暗くなるかは簡単にイメージできると思いますが、一応図として載せます。
面がライトのある方向を向いているほうが明るく、逆であるほど暗くなります。ということはこの面の方向を頂点に置き換えても同様なことになります。この向きのことを「法線」と呼びます。
さて、ボックスに設定する法線の設定は2通りあります。下のような感じです。
左と右ではライトを当てたときに違いが出てきます。
左の方法だと面と面の間が角ばって見えるようになります。これは完全に面の法線と一緒の方向を向いているからです。ただし、この方法だと頂点を共有できないというデメリットがありますが、表現方法によってはこのようにしなくてはいけないときもあります。
右の方法だと面と面の間がライトの当て方によって若干まるまって見えるようになります。頂点を共有するのでデータ量が減るというメリットがあります。デメリットは、頂点の法線が面の方向と一緒ではないため、例えば真上からライトをあてても上の面が100%ライトの影響を受けられなくなります。
文で説明してもわかりづらいので、下の図で違いを見てください。
メタセコイアというモデリングソフトで表示したものです
かなり違うことがわかります。
今回は、頂点をわざわざ分割するのが面倒なので、右の方法を使用することにします。
/// <summary>
///
/// </summary>
private Material _material = new Material();
|
ボックス用のマテリアルを宣言しておきます。マテリアルは常に保持する必要は無く、描画時に毎回作成してセットする方法でもかまいません。用途に応じて使い分けてください。
this._vertexBuffer = new VertexBuffer(typeof(CustomVertex.PositionNormal),
8, this._device, 0, CustomVertex.PositionNormal.Format, Pool.Managed);
|
コメントのとおり、「法線」が必要なので「位置」と「法線」を含む「CustomVertex.PositionNormal」構造体を使用しています。今回ライトの影響を見やすくするために「色」データははずしました。
色に関してはマテリアルで代用できるのでそちらを使います。
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;
this._device.Lights[0].Enabled = true;
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」にセットするようにしています。これにより、これ以降の描画するポリゴンにマテリアルが適用されるようになります。
あとはセットする頂点フォーマットが違うだけで基本的に変わりはありません。
|