2.5Dモーションブラー★ 1.はじめに
Kさんから,「XNA Game Studioの方でブラーを使ってみたいから,コード書いて! 」というリクエストがあったので,XNA Game Studioの方で2.5Dモーションブラーやってみました。
★ 2.2.5Dモーションブラーとは?
2.5Dモーションブラーについては,西川さんの記事に詳細が載っています。またDirectXをお使いの方はもんしょさんがすでに説明されていたり,Imagireさんが書かれている"DirectX 9 シェーダープログラミングブック"の467ページあたりにも説明がのっていますので,そちらのほうを参考にしてください。
2.5Dモーションブラーは2003年のGame Developers ConferenceでNVIDIAのSimon Greenさんが発表した"NVIDIA Stupid OpenGL Shader Tricks"が基です。 基本的な考え方は,基となる物体を描画して,それらを法線ベクトルの方向に引き伸ばし,速度マップを描画します。最後に速度マップに基づいてブラー処理を行います。 まず,シーンをテクスチャに描画します。これは問題ないでしょう。いつもどおりに描画するだけです。 次に頂点シェーダーを使って,各ピクセルに対して速度を計算し,速度マップを作成します。 ![]() まず,モデルを引き伸ばす方法ですが,移動軌跡ボリュームという方法を使います。 現在の位置座標P(t)と,1フレーム前の位置座標P(t-1)がわかれば,各頂点ごとの速度dP/dtを求めることができます。そこで,速度と法線ベクトルの向きの比較を行い,速度と法線ベクトルのむか等しければ,現在の位置座標を,向きが反対であれば1フレーム前の位置座標を使用するメッシュを作成します。向きの判定は速度ベクトルと法線ベクトルの内積を計算すれば判定ができます。 あとは,この処理を行った結果を速度マップとして出力します。実際にはつぎのフラグメントシェーダの処理で,すぐ使うのでまともにテクスチャに保存する必要はありません。 あとは,求めた速度を用いてフラグメントシェーダ(ピクセルシェーダ)で,速度ベクトルの方向に沿って,オブジェクトが書かれたテクスチャを何度もサンプリングします。最終的に出力する色はサンプリングしたテクセルを平均した色になります。 ★ 3.XNA Game Studioでの実装
さて,肝心の実装ですが,シェーダ側のコードは,OpenGLで書いた時の説明を見てください。あんまし変わりません。やっぱし,わかりづらいのは,C#側での処理だと思うのでC#のコードの方を説明していきます。
まず,プログラムの流れですが… (1) レンダーターゲットを作成する (2) 深度ステンシルバッファを作成する (3) レンダリングターゲットをテクスチャに設定する (4) 普通に描画 (5) レンダリングターゲットを通常時に戻す (6) テクスチャをGPUに転送 (7) 1pass目:引き延ばして,速度マップ作成 (8) 2pass目:速度マップを元にブラーを付ける…といった具合です。 レンダーターゲットと深度ステンシルバッファの作成ですが,LoadContent()メソッドで行いました。下のコードのような感じです。引数は適当に指定したので,気に入らなければ各自でうまく指定してください。 00136: //レンダーターゲットの作成 00137: renderTarget = new RenderTarget2D(GraphicsDevice, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height, 1, SurfaceFormat.Color); 00138: 00139: //深度ステンシルバッファの作成 00140: depthStencilBuffer = new DepthStencilBuffer(GraphicsDevice, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height, DepthFormat.Depth24Stencil8, MultiSampleType.None, 0); 00141:続いて描画ですが,Draw()メソッドは下のような感じです。
00219: /// <summary>
00220: /// This is called when the game should draw itself.
00221: /// </summary>
00222: /// <param name="gameTime">Provides a snapshot of timing values.</param>
00223: protected override void Draw(GameTime gameTime)
00224: {
00225: // TODO: Add your drawing code here
00226:
00227: //退避
00228: DepthStencilBuffer oldDepthStencilBuffer = GraphicsDevice.DepthStencilBuffer;
00229:
00230: //行列を計算
00231: prevWorldViewProjection = worldViewProjection;
00232: worldViewProjection = world * view * projection;
00233:
00234: /// Pass1: 通常描画
00235: GraphicsDevice.DepthStencilBuffer = depthStencilBuffer;
00236: GraphicsDevice.SetRenderTarget(0, renderTarget);
00237: GraphicsDevice.Clear(Color.CornflowerBlue);
00238: DrawNormalModel(model);
00239:
00240: /// Pass2: 速度マップを作成し,ブラーを描画
00241: GraphicsDevice.DepthStencilBuffer = oldDepthStencilBuffer;
00242: GraphicsDevice.SetRenderTarget(0, null);
00243: srcTexture = renderTarget.GetTexture();
00244: GraphicsDevice.Clear(Color.CornflowerBlue);
00245: DrawBlurredModel(model);
00246:
00247: base.Draw(gameTime);
00248: }
ま,書いてあるまんまですね。GraphicsDeviceに深度ステンシルバッファおよびレンダーターゲットを設定します。で,普通に描画するだけ。それで,次に書くときはテクスチャに描画したシーンを元に速度マップを作成し,ブラーを付けて描画します。 肝心のDrawNormalModel()メソッドとDrawBlurredModel()メソッドですが,単にGPUにパラメータを渡しているだけのメソッドで大したことはしてません。
00153: /// <summary>
00154: /// モデルの通常描画
00155: /// </summary>
00156: /// <param name="model">描画したいモデル</param>
00157: private void DrawNormalModel(Model model)
00158: {
00159: //Matrix[] transforms = new Matrix[model.Bones.Count];
00160: //model.CopyAbsoluteBoneTransformsTo(transforms);
00161:
00162: foreach (ModelMesh mesh in model.Meshes)
00163: {
00164: foreach (Effect effect in mesh.Effects)
00165: {
00166: effect.CurrentTechnique = effect.Techniques["Render"];
00167: effect.Parameters["worldViewProjection"].SetValue(worldViewProjection);
00168: effect.Parameters["lightPos"].SetValue(light);
00169: effect.Parameters["eyePos"].SetValue(eye);
00170: }
00171: mesh.Draw();
00172: }
00173: }
00174:
00175: /// <summary>
00176: /// ブラーを付けてモデルを描画
00177: /// </summary>
00178: /// <param name="model"></param>
00179: private void DrawBlurredModel(Model model)
00180: {
00181: //Matrix[] transforms = new Matrix[model.Bones.Count];
00182: //model.CopyAbsoluteBoneTransformsTo(transforms);
00183:
00184: foreach (ModelMesh mesh in model.Meshes)
00185: {
00186: foreach (Effect effect in mesh.Effects)
00187: {
00188: effect.CurrentTechnique = effect.Techniques["MotionBlur"];
00189: effect.Parameters["worldViewProjection"].SetValue(worldViewProjection);
00190: effect.Parameters["prevWorldViewProjection"].SetValue(prevWorldViewProjection);
00191: effect.Parameters["lightPos"].SetValue(light);
00192: effect.Parameters["eyePos"].SetValue(eye);
00193: effect.Parameters["blurScale"].SetValue(blurStrongness);
00194: effect.Parameters["SrcMap"].SetValue(srcTexture);
00195: }
00196: mesh.Draw();
00197: }
00198: }
00199:
★ 4.縮退ポリゴン
今回も時間がなくて,縮退ポリゴンを作成するツールを作れませんでしたが,暇なときに作れるように一応まとめておこうと思います。
西川さんの記事を見てもらうとわかりますが,2.5Dモーションブラーにはいくつか問題があります。そのひとつがストレッチが破綻するというものです。 わかりづらいので,図で説明。 例えば,箱があります。これを法線方向に引き延ばしたとすると,つぎはぎ部分が開いてしまいます。これがストレッチの破綻というやつです。 2.5Dモーションブラーでは法線方向ではなく,現在の位置と前の位置を計算して,引き延ばしますが当然同じようなことは起こりえます。 ![]() 「じゃ,ストレッチを破綻させない方法はあるの?」という話になってくるわけです。 ここで,ストレッチの破綻を回避する方法が,シャドウボリュームなんかでよく使われる"面積0のダミーポリゴン"を埋め込む縮退ポリゴンというやつです。 では,どういう風に縮退ポリゴンをつくればいいのか?…それは,ちょうど西川さんのページCAPCOMさんがにCEDEC2006で発表されたスライドのスクリーンショットにあります。 「同じ辺を共有し,かつ法線方向の異なる辺に対してポリゴンを事前に生成」すればいいようです。 ![]() で,ちょっとポリゴン数は多くなりますが,共有辺に対してポリゴンを埋め込むやり方がimagireさんのサイトに載っていたので,これをもとにキューブに縮退ポリゴンを埋め込むサンプルプログラムを作ってみました。 ダミーポリゴンを埋め込む具体的な方法ですが,例えば頂点1と頂点2をつなぐ辺が共有辺であり,法線方向が異なる辺だったと仮定します。 この場合,縮退ポリゴンのインデックスは,1-2-2と1-1-2になります。ここで大事なのが,頂点に与える法線ベクトルです。スムージングを考えなければ,同じ面であれば同じ面の法線ベクトルを3つに与えますが,縮退ポリゴンの場合は異なる面の法線ベクトルを1つ与えます。言葉で言うとわかりづらいので,次のコードを見てください。 00196: // 稜線にポリゴンを埋め込む 1枚目 00197: m_pVertices[3*face+0].p = pVertices[pIndices[id[1][0]]].p; 00198: m_pVertices[3*face+2].p = pVertices[pIndices[id[0][1]]].p; 00199: m_pVertices[3*face+1].p = pVertices[pIndices[id[0][0]]].p; 00200: m_pVertices[3*face+0].n = vNormal[i]; 00201: m_pVertices[3*face+2].n = vNormal[j]; 00202: m_pVertices[3*face+1].n = vNormal[i]; 00203: m_pVertices[3*face+0].vi = pIndices[id[1][0]]+1; 00204: m_pVertices[3*face+2].vi = pIndices[id[0][1]]+1; 00205: m_pVertices[3*face+1].vi = pIndices[id[0][0]]+1; 00206: m_pVertices[3*face+0].ni = i+1; 00207: m_pVertices[3*face+2].ni = j+1; 00208: m_pVertices[3*face+1].ni = i+1; 00209: face++; 00210: 00211: // 稜線にポリゴンを埋め込む 2枚目 00212: m_pVertices[3*face+0].p = pVertices[pIndices[id[1][0]]].p; 00213: m_pVertices[3*face+2].p = pVertices[pIndices[id[1][1]]].p; 00214: m_pVertices[3*face+1].p = pVertices[pIndices[id[0][1]]].p; 00215: m_pVertices[3*face+0].n = vNormal[i]; 00216: m_pVertices[3*face+2].n = vNormal[j]; 00217: m_pVertices[3*face+1].n = vNormal[j]; 00218: m_pVertices[3*face+0].vi = pIndices[id[1][0]]+1; 00219: m_pVertices[3*face+2].vi = pIndices[id[1][1]]+1; 00220: m_pVertices[3*face+1].vi = pIndices[id[0][1]]+1; 00221: m_pVertices[3*face+0].ni = i+1; 00222: m_pVertices[3*face+2].ni = j+1; 00223: m_pVertices[3*face+1].ni = j+1; 00224: face++;上のコードで言うところの201〜203と221〜223が該当箇所です。 このように処理しないと,ストレッチが破綻してしまいますので注意しましょう。いちおう上のサンプルのプロジェクトをここにアップしておきます。中身はテスト用に作ったのでいい加減なものなので,参考程度にとどめておいてください。 ★ Download
本ソースコードおよびプログラムを使用したことによる如何なる損害も製作者は責任を負いません。
本ソースコードおよびプログラムは自己責任でご使用ください。 プログラムの作成にはMicrosoft Visual Studio 2008 SP1 Professional, XNA Game Studio 3.1を用いています。 |