カウンタ

  ライトで陰を付ける

Google
▲探し物はこちら

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

ライトで陰を付ける

 下のリンクから今回のプロジェクトをダウンロードできます。

ファイル名 言語 サイズ バージョン
lighting_cs_1_1.zip C# 25KB 1.1
lighting_vb_1_1.zip VB.NET 31KB 1.1
lighting_cpp_1_1.zip C++/CLI 14KB 1.1

 今回のメインコードファイルを載せます。重要なコードを赤色で表示させています

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 ファイルのコードはこちらです。


 コードの説明に入る前に補足説明を入れておきます。

 今回ライトに関してのサンプルになりますが、ここで使われるキーワードとして「マテリアル」「ライト」「法線」があります。

マテリアル

 まず、マテリアルについてですが、マテリアルとは簡単に言い換えれば「物質の色」です。基本的に個々の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」にセットするようにしています。これにより、これ以降の描画するポリゴンにマテリアルが適用されるようになります。
 あとはセットする頂点フォーマットが違うだけで基本的に変わりはありません。

その他の関連情報です▼