ライトの利用に入ろうと思うのですが、今までライトなんて利用してきませんでしたよね?では、どうしてものが見えたのでしょうか?これは意外と単純で今までは物体の色をそのまま表示していたのです。シミュレーションされた世界とは都合の良いもので、人間の都合に合わせて改ざんされます。要するに「光源を設定しない2次元」と同じ感覚で「あえて光源を設定しない3次元」を扱っていたのです。まあ、どうでも良いことかもしれませんね。とにかくこれからライトを使うと言うことです。
利用するためには設定をしなければなりません。と、言いたいところですが標準でライトは使うことになっています。要するに今まではあえてオフにしていたと言うことです。何処でそれをしていたか分かりますか?ちょっとソースコードを見直してみてください。
device_.RenderState.Lighting=false;
ちょっと正解を話すのが早いかもしれませんが、サクサク進ませて頂きます。
名前のまんまですね。「Lighting」なんて、全くひねりがありません。RPGの魔法でも、もっとましな技名にするでしょう。
というわけでここの行は削除します。あえて強調したい方はTreuにしたものを書き加えても構いません。
ここでライトの設定にすんなりと入りたいところなのですが、そうは問屋が卸しません。とりあえず言っておきますが、今回扱う光は「環境光」と「ディフューズ光」です。
前回のお話を隅から隅まで読んだ方はは法線と言う言葉を見たと思います。この法線。ちょっと厄介かもしれません。
面には向きがありますよね?たとえば机の表面は上を向いています。そして面の向きとディフューズ光は深い関係があります。(アンビエント光は関係ありませんよ)
面が一番明るいのは光がどういう角度で面にぶつかるときでしょうか?(距離は近い方が良いに決まってます)
これは当然光が垂直に当たるときですね。つまり面の向きと光の向きが反対であるほどその面は明るくなるということです。(現実世界では正しくないですが、シミュレーション世界では正しいのです)
というわけで面の向きが必要になるのですが、「面の向きってどうやって決まるのでしょうか?」たとえば下の図のように決まるのでしょうか?
実はこれではないですし、これでは困るのですよ。たとえば、曲面を作るとします。そうすると、ものすごく小さな三角形を作らないと本物っぽい曲面に見えなくなってしまいます。
注…実際には面に垂直な法線を利用しているわけではありません。あくまでイメージ画です。
そういうわけで、面の向きは周りの頂点に設定します。
ちょっと脇道にそれますが、2,3言いたいことを。
法線はベクトルです。そして、ベクトルである以上「長さ」も持っています。ですので、長さが必要ないときは正規化しましょう。
先ほどの球は実際には面を作っている一番最初の頂点の法線ベクトルを利用しています。そのため、四角形の形で色の同じところが出てくるのだと思います。
それと何で球を作るときに、頂点に法線を持たせると良いのかというと、「線形補完できる」からです。最低三つの頂点があるのでその三つの法線を利用すれば・・・ね。
ベクトルが分かる方にはここの説明はつまらない物だと思います。斜め読みでも大丈夫でしょう。それと、私はベクトルについて余りよく分かっていません。もし変な箇所があれば指摘して頂けるとありがたいです。
Vector3というのがありましたよね?この「Vector」、ベクトルという意味があります。
それで、これは位置を表すのに使っていたのですが、これは法線を表すのにも使います。つまり(1,1,1)と言う値は
と言うことになります。
ベクトルを知らない方には何点か注意を。
まず、「原点から~」と言いましたが、あくまで方向ですので「頂点座標が(2,3,4)で、法線が(1,1,1)だったら(2,3,4)から見て、(3,4,5)の方向のことになります。
もう一点は(1,1,1)と(2,2,2)の違いと同じ所です。これらは方向は同じですが、長さが違います。それで、ちょっとした決まり事して、方向だけを扱うときは長さが1になるようにするということがあるようです。そしてこの方向と長さを持った物(以下「ベクトル」と表現します)から、方向は変えず長さを1にすることを正規化と言います。
先ほどの(1,1,1)を正規化すると(1/sqrt(3),1/sqrt(3),1/sqrt(3))になります。
そういうわけで、法線ベクトルを含んだ頂点データを作ります。さて、頂点はなんこ使えばいいでしょうか?
一番簡単なのは、8個の頂点を作って外側に向かうように法線ベクトルを設定する方法だと思います。
手前右上と奥左下の矢印が抜けていますが、こういう感じです。でも、さっき言いましたよね、「面のある一点の法線ベクトルは周りの頂点を使って線形補完される」と。つまり、平面のはずの立方体で光の当たり方が変になる。ということです
左は真横から光を当てた様子。右は斜め(x,y,zで少しずつ方向をずらしてある)から光を当てたところです。まあ、こんな立方体があっても良いですが、変じゃないですか?
私が会って欲しい立方体はこれです。実際には、もうちょっと環境光を効かせて真っ黒な面がでないようにしますが、このようにちゃんと面の色がはっきりしてる方が立方体っぽく思えませんか?
この立方体。実は36頂点使っています。つまり、こういう事です。
それぞれの面を作る頂点が、それぞれ正しい方向の法線ベクトルを持っているのです。
36頂点っていうのはかなり多い数ですよね。そういうわけで、あまり多くの頂点を作りたくないときにはこの方法はおすすめできません。むしろ、8頂点の方でごまかしをかける方が良いかもしれません。
もう一つ。シェーディングモードをフラットにすれば、8頂点でしっかりとした立方体を作れるかもしれません。(私はまだ試していません)どなたか挑戦してみませんか?
長いと何処が重要か分からなくなってきますが、読んでいて分からないところが重要なところです。しっかりと目を通しましょう。
using System; using System.Drawing; using System.Windows.Forms; using Microsoft.DirectX; using Microsoft.DirectX.Direct3D; namespace Project2 { /// <summary> /// md3d2 の概要の説明です。 /// </summary> public class md3d2:Form { public md3d2():base() { //最小サイズを設定 this.MinimumSize=new Size(80,60); this.Size = new Size(300,300); //ウィンドウの名前(かっこいいのを付けてあげてください) this.Text ="Direct3D-My"; } private Device device_; private PresentParameters presentParam_; /// <summary> /// Direct3Dの初期化を行います。 /// </summary> /// <returns>初期化が成功したかどうか</returns> public bool DXInitialize() { try { //プレゼンテーションパラメータを作成 presentParam_ = new PresentParameters(); //ウィンドウモード presentParam_.Windowed =true; //スワップエフェクトを設定。 presentParam_.SwapEffect = SwapEffect.Discard; //デバイスを作成 device_ = new Device(0,DeviceType.Hardware,this ,CreateFlags.HardwareVertexProcessing,presentParam_); creatVertex(); //初期化成功 return true; } catch { //初期化失敗 return false; } } /// <summary> /// 頂点バッファ /// </summary> private VertexBuffer vertexBuffer_; /// <summary> /// 頂点バッファ作成関数 /// </summary> private void creatVertex() { //頂点バッファ領域を確保 vertexBuffer_ = new VertexBuffer( typeof(CustomVertex.PositionNormal),36, device_ , 0, CustomVertex.PositionNormal.Format, Pool.Managed); //頂点データの配列を作成 CustomVertex.PositionNormal[] verts = new CustomVertex.PositionNormal[36]; //頂点データ verts[0].Position =new Vector3(-1, 1,-1); verts[1].Position =new Vector3( 1,-1,-1); verts[2].Position =new Vector3(-1,-1,-1); verts[3].Position =new Vector3(-1, 1,-1); verts[4].Position =new Vector3( 1, 1,-1); verts[5].Position =new Vector3( 1,-1,-1); verts[6].Position =new Vector3(-1, 1, 1); verts[7].Position =new Vector3(-1,-1, 1); verts[8].Position =new Vector3( 1,-1, 1); verts[9].Position =new Vector3(-1, 1, 1); verts[10].Position =new Vector3( 1,-1, 1); verts[11].Position =new Vector3( 1, 1, 1); verts[12].Position =new Vector3(-1, 1,-1); verts[13].Position =new Vector3(-1,-1,-1); verts[14].Position =new Vector3(-1,-1, 1); verts[15].Position =new Vector3(-1, 1,-1); verts[16].Position =new Vector3(-1,-1, 1); verts[17].Position =new Vector3(-1, 1, 1); verts[18].Position =new Vector3( 1, 1,-1); verts[19].Position =new Vector3( 1,-1, 1); verts[20].Position =new Vector3( 1,-1,-1); verts[21].Position =new Vector3( 1, 1,-1); verts[22].Position =new Vector3( 1, 1, 1); verts[23].Position =new Vector3( 1,-1, 1); verts[24].Position =new Vector3( 1,-1,-1); verts[25].Position =new Vector3(-1,-1, 1); verts[26].Position =new Vector3(-1,-1,-1); verts[27].Position =new Vector3( 1,-1,-1); verts[28].Position =new Vector3( 1,-1, 1); verts[29].Position =new Vector3(-1,-1, 1); verts[30].Position =new Vector3( 1, 1,-1); verts[31].Position =new Vector3(-1, 1,-1); verts[32].Position =new Vector3(-1, 1,1); verts[33].Position =new Vector3( 1, 1,-1); verts[34].Position =new Vector3(-1, 1, 1); verts[35].Position =new Vector3( 1, 1, 1); verts[0].Normal =new Vector3( 0, 0,-1); verts[1].Normal =new Vector3( 0, 0,-1); verts[2].Normal =new Vector3( 0, 0,-1); verts[3].Normal =new Vector3( 0, 0,-1); verts[4].Normal =new Vector3( 0, 0,-1); verts[5].Normal =new Vector3( 0, 0,-1); verts[6].Normal =new Vector3( 0, 0, 1); verts[7].Normal =new Vector3( 0, 0, 1); verts[8].Normal =new Vector3( 0, 0, 1); verts[9].Normal =new Vector3( 0, 0, 1); verts[10].Normal =new Vector3( 0, 0, 1); verts[11].Normal =new Vector3( 0, 0, 1); verts[12].Normal =new Vector3(-1, 0, 0); verts[13].Normal =new Vector3(-1, 0, 0); verts[14].Normal =new Vector3(-1, 0, 0); verts[15].Normal =new Vector3(-1, 0, 0); verts[16].Normal =new Vector3(-1, 0, 0); verts[17].Normal =new Vector3(-1, 0, 0); verts[18].Normal =new Vector3( 1, 0, 0); verts[19].Normal =new Vector3( 1, 0, 0); verts[20].Normal =new Vector3( 1, 0, 0); verts[21].Normal =new Vector3( 1, 0, 0); verts[22].Normal =new Vector3( 1, 0, 0); verts[23].Normal =new Vector3( 1, 0, 0); verts[24].Normal =new Vector3( 0,-1, 0); verts[25].Normal =new Vector3( 0,-1, 0); verts[26].Normal =new Vector3( 0,-1, 0); verts[27].Normal =new Vector3( 0,-1, 0); verts[28].Normal =new Vector3( 0,-1, 0); verts[29].Normal =new Vector3( 0,-1, 0); verts[30].Normal =new Vector3( 0, 1, 0); verts[31].Normal =new Vector3( 0, 1, 0); verts[32].Normal =new Vector3( 0, 1, 0); verts[33].Normal =new Vector3( 0, 1, 0); verts[34].Normal =new Vector3( 0, 1, 0); verts[35].Normal =new Vector3( 0, 1, 0); //バッファをロック GraphicsStream stm = vertexBuffer_.Lock(0,0,0); //頂点データをバッファに書き込み stm.Write(verts); //バッファのロックを解除 vertexBuffer_.Unlock(); } public void Render() { if(device_==null)return; if(this.WindowState ==FormWindowState.Minimized)return; //カメラの設定を行う device_.Transform.View = Matrix.LookAtLH( new Vector3( 5.0f, 5.0f, 5.0f ), new Vector3( 0.0f, 0.0f, 0.0f ), new Vector3( 0.0f, 1.0f, 0.0f ) ); device_.Transform.Projection = Matrix.PerspectiveFovLH((float)Math.PI / 4, (float)this.ClientSize.Width / (float)this.ClientSize.Height, 3.0f, 100.0f); //device_.RenderState.Lighting =false; //ライトの設定 device_.Lights[0].Direction =Vector3.Normalize(new Vector3(-1,-2,-3)); device_.Lights[0].Type = LightType.Directional; device_.Lights[0].Diffuse = Color.FromArgb(255,255,255); device_.Lights[0].Ambient = Color.FromArgb(40,40,40); device_.Lights[0].Enabled =true; device_.Lights[0].Update(); device_.Clear(ClearFlags.Target,Color.Blue,1.0f,0); device_.BeginScene(); //マテリアルの設定 Material mat =new Material(); mat.AmbientColor = new ColorValue(1.0f,1.0f,1.0f); mat.DiffuseColor = new ColorValue(1.0f,1.0f,1.0f); device_.Material = mat; device_.SetStreamSource(0,vertexBuffer_,0); device_.VertexFormat = CustomVertex.PositionNormal.Format; device_.DrawPrimitives(PrimitiveType.TriangleList,0,12); device_.EndScene(); try { //更新 device_.Present(); } catch(DeviceLostException) { resetDevice(); } } /// <summary> /// デバイスのリセットを行う /// </summary> private void resetDevice() { int result; if(!device_.CheckCooperativeLevel(out result)) { if(result ==(int)ResultCode.DeviceLost) { System.Threading.Thread.Sleep(10); } else if(result ==(int)ResultCode.DeviceNotReset) { device_.Reset(presentParam_); } } } } }
エントリポイントがありませんが、いつものでお願いします。
法線ベクトルをセットしています。「Normal」の部分ですね。大抵は正規化をかけるのですが、今回はベクトルの長さが1になってしまうのでかけていません。無駄ですからね。
いよいよライトの説明です。まずはライトの種類から。ライトには4つの種類があります。(絵心の無さは気にしないで下さい)
これはの三つとは違い、「Device.RenderState.Ambient」で設定します。
注意…ここの環境光というのはあくまで世界全体に適応される環境光の設定です。下の三つの光源にも環境光を設定する項目があります。
光源がすごく遠くにあるという仮定の下に使われるライトです。光は放射状ではなく平行になっています。
この光には到達限界が無く、光の減衰もしません。
点光源は光が放射状に広がる光源です。
座標が必要です。それと、光には到達限界があり、減衰させることが可能です。
点光源は平行光光源より負荷が大きいです。たくさん使うと重くなってしまいます。
スポットライトは点光源の方向限定版です。それと、照らす範囲の大きさを角度で指定しますが、「内部コーン」と「外部コーン」があります。外部コーンの外側は完全に光が届きません。反対に内部コーンの中では設定した減衰に完全に従います。そして外部コーンから内部コーンにかけての部分の所について光の変化が設定できるようになっています。
座標と方向が必要です。点光源と同じく、光には到達限界があり、減衰させることが可能です。
スポットライトは、点光源よりもさらに負荷が高いです。
ライトは、「Device.Lights」に配列として用意してあります。とりあえず0番から使うようにしましょう。環境光以外は、どのライトを使うかの種類を設定しなければなりません。「Type」に「LightType列挙型」を使って指定してください。
前回話した通り、光は種類ごとに色を設定します。今回は「環境光」と「ディフューズ光」を利用します。
設定の仕方は、2種類有り、「~」と「~Value」とあります。「~」の方は「Color構造体」をセットします。「~Color」は「ColorValue構造体」をセットします。両方は繋がっているようなので、どっちでセットしても良いですが、「~Color」の方は最大値がfloat型の限界かもしれません。そのため、こちらの方が自由度が高いです。
スペキュラー光を使うためには、もう一つ設定をする必要があります。
Device.RenderState.SpecularEnable
ここをTrueに設定する必要があります。負荷が高くなるので気を付けてください。
今回使っているのは平行光源のため、光の向きだけで足りますが、点光源とスポットライトは位置が必要になります。方向は「Direction」です。これも法線と同じくベクトルのため、正規化が必要になります。位置の方はそのままワールド座標で指定します。
ライトを有効にするためには、「Enabled」を「True」にする必要があります。これをしないと、ライトが有効になりません。それと、全ての設定が終わったら、「Update」メソッドを呼び出してください。これは、設定したパラメーターを有効にするために必要です。決まり事ですので忘れずに。
マテリアルの方は、そのまんまです。難しいことはないのでそれぞれぞれの光について色を設定してください。これも「~Color」の方が自由度が高いと思います。それと、Deviceのほうで直接弄ろうとするとコンパイルエラーが起こるので、一回newして設定したものをセットしてください。
座標や、減衰率を色々と変えながら試すと良いと思います。細かな設定については「Direct3D Tips ライト(Light)」に載っていることが参考になるかもしれません。他の光源については、後々利用することがあると思います。自分で色々と試してみるとおもしろいです。
次は「メッシュ」についてです。ライトについてはもっと話そうと思ったのですが、どうもライトはあまり人気がないようです。やっぱりたくさん光源を使うとそれだけ計算が増えるので、それよりはむしろ「ピクセルシェーダー」のほうを利用するようです。でも、簡単に陰影を付けたいときにはこっちの方が楽だから覚えておいて損はないと思います。