XNA 3D:カスタムシェーダーの紹介と少しのプロトタイプ





こんにちは、Habrahabr! 残念なことに、私は非常に長い間、私はに書きませんでした 個人的な問題は、gamedevに座っていくつかの記事を書くことに完全に反対しました。 たぶんそれは最高だろう、この2年間で私は多くの経験を積んできたし、それをいつも喜んで共有している。 2Dゲームの作成を完全に拒否したことは注目に値します。2Dゲームに反対するわけではありませんが、3Dでゲームを開発する方がはるかに面白くて楽しいです! 伝統的に、 XNA 4.0はツールになりますが、なぜXNA 4.0は高価なリスナーですか? そして、それはすべて、インディー開発者にとって依然として関連性が高いためです。 出現頻度が非常に低い言語(C#)があります。 必要な初期クラス/構造およびアルゴリズムを備えた同じXNAフレームワークがあります。 また、 シェーダーモデル3.0までのシェーダーをサポートするDirectXがあります。 %username%を初めて読んだ場合、2012年の私の記事を同時に読むことができます。 それらが100%関連しているとは限らず、エラーがないとは限りませんが、一定の根拠を与えることができます。 おそらく、それは明らかです-私は3Dについてのみ書くつもりです:トピックのリストを最後まで決めていませんが、私はかなり早くそれらを形成すると思います。



これまでのところ、2つの記事を正確に思いついています。





次に、 カスタムシェーダーを紹介し、 FEZゲームの簡単なプロトタイプを実装します。





はじめに



2Dで作業したとき、マトリックスをせず 、テクスチャとその位置をローカルのSpriteBatchに渡し 、彼がペイントしてくれました。 しかし、彼が描くものはすべて3Dであると言いたいです。座標の1つだけがゼロ( XNAではZ座標)であり、まあ、特別な投影が使用されます(それらについては少し後で)。 投影-3D空間から2Dスクリーン空間への座標の変換。 また、 SpriteBatchメソッドのオーバーロードの1つは、マトリックスの形式のパラメーターをサポートしています。これにより、カメラを作成しました。 そして今、私たちは3Dとの類推を与えます。 テクスチャの位置(およびその回転とサイズ)としてSpriteBatchに渡した座標-これは、 ワールド変換 (およびワールドマトリックス)と呼ばれます。 SpriteBatch.Beginのマトリックスパラメーターは、 ビューマトリックスです。 そして、 SpriteBatchでは変更できない特別な投影マトリックス。 そして今、また別の角度から:モデルは点で構成されます- 頂点と呼ばれ、これらの頂点の全体の位置(変換)はローカル座標系です。 次に、これのおかげでそれらをグローバルにする必要があります-同じモデルを使用し、異なる場所で異なる回転/サイズで描画できます。 その後、カメラに合わせてこの変換をシフトする必要があります。 そして、最終的な変換を計算した後、それらを3D空間からスクリーン2Dに投影します。



グラフィックデバイス:シェーダーとモデル



過去の記事でシェーダーのトピックと、それらを使用して画像の後処理を行う方法について触れました。 ピクセルシェーダーのみを使用しました。 実際、すべてがより複雑です。 ピクセルシェーダーに加えて、頂点シェーダーがあります( SM3.0までの状況を検討します )。 これらのシェーダーは、ピクセルではなく頂点で動作します。 つまり 各頂点に対して実行されます。 ここに、変換の魔法があります。 XNAで新しい.fxファイルを作成して解析してみましょう。



float4x4 World; float4x4 View; float4x4 Projection;
      
      







最初の3行は単なるマトリックスです。 これらの値はすべて、いわゆる定数バッファー (少し後のバッファーについて)から取得されます。 次は、入出力構造の実装です。



 struct VertexShaderInput { float4 Position : POSITION0; }; struct VertexShaderOutput { float4 Position : POSITION0; };
      
      







これは、頂点シェーダー入出力の最も単純な実装です。POSITION0チャネルの頂点位置を取得し、出力として、グラフィックパイプラインの次の部分(変換済みのデータ)の情報をラスタライザーに報告します。

さて、 .fxファイルの最後の部分はシェーダー自体です。



 VertexShaderOutput VertexShaderFunction(VertexShaderInput input) { VertexShaderOutput output; float4 worldPosition = mul(input.Position, World); float4 viewPosition = mul(worldPosition, View); output.Position = mul(viewPosition, Projection); return output; } float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0 { return float4(1, 0, 0, 1); }
      
      







頂点シェーダーは、「モデル」空間内の頂点の位置を取得し、ワールド、ビュー、そして最終的に画面につながります。 さて、ピクセルシェーダーはすべてを赤で塗りつぶします。



マトリックスについて少し



これらのマトリックスの詳細には触れません(同様の情報がたくさんあります:それは非常に一般的だからです):私はこれがXNAでどうなるかだけを言います。



ワールドマトリックスは、 Scale Rotation Translationビューによって定義されます。



 Matrix world = Matrix.CreateScale(x, y, z) * Matrix.CreateFromYawPitchRoll(y, p, r) * Matrix.CreateTranslation(x, y, z);
      
      







行列の乗算は非可換であるため、ここでは順序が重要です。

ビューマトリックス(カメラマトリックス):



 Matrix view = Matrix.CreateLookAt(vpos, targetpos, up);
      
      







一般的に、 SRTマトリックスを使用できますが、 XNAにはそれを簡単にする便利な方法があります。

Vposはカメラの位置、 targetposはカメラが見ているポイント、 up上向きのベクトル(通常はVector3.Up )です。



さて、最後の最も重要なことは、射影行列です。 最も標準的な場合には、 射投影と遠近 法の 2つがあります 。 将来的には、正射投影法は2Dに使用され(アイソメにもある程度使用されます)、他のケース(3Dシューティングゲームなど)で有望です。



画像








私たちの目は、すべてのオブジェクトを透視投影で知覚するように設計されています。 オブジェクトが遠くなるほど、私たちには見えないようになります。 オブジェクトのサイズはオブジェクトまでの距離に依存しないため、一見すると直交投影は奇妙に見えます。 無限に長い距離からシーンを見たようなものです。 それが、2Dゲームに使用される理由です。 特定のポリゴンまでの距離を考慮する必要はありません。



モデルビュー



ハードディスクからモデルをロードする場合-次の情報をロードします(最も単純な場合)。



POSITION-頂点の位置。

NORMALは頂点の法線です。

TEXCOORD-テクスチャ座標(UVスキャン)。



この情報は、頂点チャネルと呼ばれます (RGB空間の赤チャネルと同様)。



また、 indexと呼ばれる特別な情報をロードします 。 それでは、試してみましょう。



4つの点で構成される正方形を描画する必要があります。 グラフィックデバイスは三角形でのみ動作ます。 どの図形も三角形に分割できます。 そして今、グラフィックデバイスにこの正方形を記述する場合、 6つの頂点が必要になります(各三角形に3つ )。 しかし、この場合、いくつかの頂点(またはむしろそれらの位置)が等しくなることを置き換えたいと思います。 このために、特別なインデックスバッファが発明されました。 正方形を表す4つのサポート頂点v1、v2、v3、v4があるとします。 そして今、あなたはインデックスバッファを構築することができます: [0、1、2、1、2、3] -これらのインデックスを使用するグラフィックデバイスは、頂点バッファ[v1、v2、v3、v2、v3、v4]から頂点を見つけて使用します。 このアプローチは非常に便利です 頂点バッファのボリュームを大幅に削減し、描画機能を拡張します。 たとえば、大きな頂点バッファーを指定して(非常に遅い操作)、モデルの特定の部分を描画します-インデックスバッファーのみを変更します(頂点バッファーのインストールに比べて速い操作)。

XNAには、 VertexBufferおよびIndexBufferというクラスが存在します。 コードで直接作成することは考慮しませんが、このために単純なモデルのロードを使用します。



単純なモデルを作成し、FBX形式で保存します。









その後-作成済みのVertexBufferIndexBufferを抽出できます



 _vertexBuffer = boxModel.Meshes[0].MeshParts[0].VertexBuffer; _indexBuffer = boxModel.Meshes[0].MeshParts[0].IndexBuffer;
      
      







注意! このような場合は、変換されていないメッシュを1つ持つ最も単純なモデルにのみ適しています。 複雑なモデルには、複数の頂点/インデックスバッファーがあります。 また、モデルの一部は、モデル空間で独自の変換を持つことができます。



実装



これで準備が整いましたので、プロトタイプを作成しましょう。 このような素晴らしいインディーズゲーム-FEZがあります。 私の友人の多くは、そのようなゲームプレイが実装の面でどのように可能であるかを尋ねましたか? 実際、単純なものではなく、世界を回転させる機能を備えた直交(正投影)投影(および古典的な2Dゲームとは異なり、世界に関する情報は3Dに含まれています)を使用するだけです。



ウィキペディアのゲームプレイの説明:

Fezは、Gomezがオブジェクトを歩いたり、ジャンプしたり、登ったり、操作したりできる2Dプラットフォーマーとして提供されます。 それでも、プレイヤーはいつでも視点をシフトし、画面に対して世界を90度回転させることができます。 これにより、ドアや通路を検出でき、プラットフォームを再構築できます。 ボリュームは2Dゲームの特徴ではないため、プレーヤーはこのメカニズムを使用して、実際の3Dの世界では通常不可能なアクションを実行できます(また、そうしなければなりません)。 たとえば、移動するプラットフォームの上に立って遠近法を90度シフトすると、Gomezは以前は画面の反対側にあった別のプラットフォームに切り替えることができます。 移動後に元の視点に戻ると、Gomezが大きく移動したことがわかります。





そして、ゲームプレイ自体のビデオ:





さあ始めましょう!


ベースジオメトリの読み込み:



 Model boxModel = Content.Load<Model>("simple_cube"); _vertexBuffer = boxModel.Meshes[0].MeshParts[0].VertexBuffer; _indexBuffer = boxModel.Meshes[0].MeshParts[0].IndexBuffer;
      
      







2つの単純な16x16テクスチャをロードします。



 _simpleTexture1 = Content.Load<Texture2D>("simple_texture1"); _simpleTexture2 = Content.Load<Texture2D>("simple_texture2");
      
      







そして、以前に作成したシェーダー(エフェクト)(.fx)をロードします。



 _effect = Content.Load<Effect>("simple_effect");
      
      







これはすべて、メソッドLoadContentで行います



そして、モデルを描画してみましょう( Drawメソッド):



シンプルなレンダリング
 //   GraphicsDevice.SetVertexBuffer(_vertexBuffer); GraphicsDevice.Indices = _indexBuffer; //   Matrix view = Matrix.CreateLookAt(Vector3.One * 2f, Vector3.Zero, Vector3.Up); Matrix projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45f), GraphicsDevice.Viewport.AspectRatio, 0.01f, 100f); float dt = (float)gameTime.TotalGameTime.TotalSeconds; Matrix world = Matrix.CreateFromYawPitchRoll(dt, dt, dt); //     _effect.Parameters["View"].SetValue(view); _effect.Parameters["Projection"].SetValue(projection); _effect.Parameters["World"].SetValue(world); //    _effect.CurrentTechnique.Passes[0].Apply(); //   GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, _vertexBuffer.VertexCount, 0, _indexBuffer.IndexCount / 3);
      
      







注意! このような場合は、1つのパスを持つ最も単純なシェーダーにのみ適しています。 複雑なシェーダーは複数のパスを持つことができます。











ここでは、視野角45度の透視投影を使用しました。 すべてが機能します。 次に、モデルのテクスチャを設定する必要があります。 モデルをCinema 4DからFBX形式に保存するとき、次のチャンネルがありました: POSITIONNORMALTEXCOORD (UV)。 シェーダーに戻り、チャンネルの1つを入力/出力データに追加しましょう。



 struct VertexShaderInput { float4 Position : POSITION0; float2 UV : TEXCOORD0; }; struct VertexShaderOutput { float4 Position : POSITION0; float2 UV : TEXCOORD0; };
      
      







これらがテクスチャ座標になります。 テクスチャ座標は、頂点を2次元テクスチャ上の位置に接続します。

そして、頂点シェーダーでは変更せずに渡します。



 output.UV = input.UV;
      
      







ラスタライズ手順の後、補間されたTEXCOORD0値(三角形に沿って)を取得し、ピクセルシェーダーでテクスチャカラー値を取得できます。



 float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0 { return tex2D(TextureSampler, input.UV); }
      
      







しかし、UVによってテクスチャの色の値を取得するには、このテクスチャを設定する必要があります。 このために、テクスチャ自体に加えて、ラスタライズされた三角形の画面上のテクスチャが大きすぎたり小さすぎたりする場合の対処方法に関する情報を含むサンプラーがあります。



なぜなら シェーダーでパラメーターを明示的に設定し、伝統を守り、シェーダーでサンプラーを作成します。



 texture Texture; sampler2D TextureSampler = sampler_state { Texture = <Texture>; };
      
      







さて、パラメータとしてテクスチャを設定しましょう:

 _effect.Parameters["Texture"].SetValue(_simpleTexture1);
      
      







見る:











しかし、以来 画面上のテクスチャは16x16以上であることが判明しました-サンプラーの設定に従って補間されました。 必要ないので、サンプラーのフィルタリングを変更します。



 texture Texture; sampler2D TextureSampler = sampler_state { Texture = <Texture>; MipFilter = POINT; MinFilter = POINT; MagFilter = POINT; };
      
      















ところで、私は以前の記事でフィルタリングについて話しまし



これですべての設定が完了しました。2Dと3Dを組み合わせるときです。 Blockクラスを紹介しましょう:



 public class Block { public enum BlockType { First, Second } public Matrix Transform; public BlockType Type; }
      
      







Transformはオブジェクトのワールドマトリックスです。



そして、これらのブロックの簡単な生成:



ブロック生成
 private void _createPyramid(Vector3 basePosition, int basesize, int baseheight, Block.BlockType type) { for (int h = 0; h < baseheight; h++) { int size = basesize - h * 2; for (int i = 0; i < size; i++) for (int j = 0; j < size; j++) { Block block = new Block(); Vector3 position = new Vector3( -(float)size / 2f + (float)i, (float)h, -(float)size / 2f + (float)j) + basePosition; block.Transform = Matrix.CreateTranslation(position); block.Type = type; _blocks.Add(block); } } }
      
      







さて、異なる変換で多くのモデルを描く能力:



レンダリング
 GraphicsDevice.SetVertexBuffer(_vertexBuffer); GraphicsDevice.Indices = _indexBuffer; Matrix view = Matrix.CreateLookAt(Vector3.One * 10f, Vector3.Zero, Vector3.Up); Matrix projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45f), GraphicsDevice.Viewport.AspectRatio, 0.01f, 100f); _effect.Parameters["View"].SetValue(view); _effect.Parameters["Projection"].SetValue(projection); foreach (Block block in _blocks) { Matrix world = block.Transform; _effect.Parameters["Texture"].SetValue(_simpleTexture1); _effect.Parameters["World"].SetValue(world); _effect.CurrentTechnique.Passes[0].Apply(); GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, _vertexBuffer.VertexCount, 0, _indexBuffer.IndexCount / 3); }
      
      







そして最後の段階-私たちは世界を創造します:



 _createPyramid(new Vector3(-7f, 0f, 5.5f), 10, 5, Block.BlockType.First); _createPyramid(new Vector3(7f, 0f, 0f), 5, 3, Block.BlockType.Second); _createPyramid(new Vector3(7f, -7f, 7f), 7, 10, Block.BlockType.First);
      
      















残された最も重要なことは、希望する投影の作成と世界を回転させる能力です。



次のように投影を設定します。



 Matrix projection = Matrix.CreateOrthographic(20f * GraphicsDevice.Viewport.AspectRatio, 20f, -100f, 100f);
      
      







20fは一種の「ズーム」です。 -100fおよび100fの近端および遠端。



種:



 Matrix view = Matrix.CreateRotationY(MathHelper.PiOver2 * _rotation);
      
      







_rotationは回転です。 「ソフト」回転の場合、 MathHelper.SmoothStep関数を使用できます。これは3次 lerpにすぎません。



回転させるために、4つの変数を導入します。



 /* ROTATION */ float _rotation; float _rotationTo; float _rotationFrom; float _rotationDelta;
      
      







そして、アップデートを更新します



回転制御
  if (keyboardState.IsKeyDown(Keys.Left) && _prevKeyboardState.IsKeyUp(Keys.Left) && _rotationDelta >= 1f) { _rotationFrom = _rotation; _rotationTo = _rotation - 1f; _rotationDelta = 0f; } if (keyboardState.IsKeyDown(Keys.Right) && _prevKeyboardState.IsKeyUp(Keys.Right) && _rotationDelta >= 1f) { _rotationFrom = _rotation; _rotationTo = _rotation + 1f; _rotationDelta = 0f; } if (_rotationDelta <= 1f) { _rotationDelta += (float)gameTime.ElapsedGameTime.TotalSeconds * 2f; _rotation = MathHelper.SmoothStep(_rotationFrom, _rotationTo, _rotationDelta); }
      
      







ここgameTime.ElapsedGameTimeが使用されていることに注目してください。ゲーム内の変数の変化が徐々に発生する場合、この値を考慮する必要があります。 FPS はすべての人で異なる場合があります



そして最後に、世界の実装に関するいくつかの言葉。 レベルの読み込み時および回転中に、各回転ごとにワールド(物理)を生成して、プレーヤーが現在の位置から目的のレベルに移動できるかどうか、およびどの位置に移動できるかを確認できます。



プロトタイプコメント



もちろん、このプロトタイプには多くの問題があります。たとえば、決して見ない顔です(ジオメトリの動的な構築について書いた場合、記事は何度も成長します)。 さらに、このすべて-BasicEffectを使用して、 カスタムシェーダーなしで実装できます。 しかし、今後の記事では、モデルにバインドせずに シェーダーカスタマイズする方法を理解することが重要です。







ソースコード+バイナリ: ここ



おわりに



この記事では、いくつかの紹介を行い、投影ではなく遠近感を使用する有名なゲームのプロトタイプを示しました。 この紹介の後、ゲーム開発の機能の一部を3次元で説明する予定です。シェーダーを詳細に(ピクセルと頂点の両方で)紹介しようと思います。 また、 HDR遅延レンダリング (2Dでこれを行うために使用していましたが、そのメソッドは遅延メソッドよりも修正された順方向レンダリング)、 VTFなどのメソッドを導入します。 私はhabrahabrのみを公開しているので、提案についてコメントするのはいつでも嬉しいです:ゲームですべてがどのように機能するかわからない場合(AAAクラスのゲームは特に歓迎です)、またはこの効果の実装に興味がある場合は、 コメントを書いてください 。 できる限り話をしようと思います。



PS私にとっての一番の動機はあなたの興味です。

PSS私たちはすべて人間であり、間違いを犯します。したがって、テキストに間違いを見つけたら、私に個人的なメッセージを書いてください。そして、怒りのコメントを急いで書いてはいけません!



All Articles