GPUによるパーティクル


~ GPU particle ~







下の画像をクリックすると、パーティクルが飛び跳ねるアニメーションが見られます。


■はじめに

また、spinネタで恐縮ですが、GPUを使ったパーティクルの時間発展のプログラムです。
最近の3D APIを、ちょっとでもさわった事のある人ならば気づいていると思いますが、 現在のグラフィックパイプラインの流れは、CPUで変換行列などを設定しておいて、 あとは最終的なフレームバッファへのレンダリングまでGPUにお任せするのが基本的な流れになっています。
つまり、データの流れは、CPUからGPUに一方的に流れるものであって、GPUからCPUへのデータの流れは考えられていません。
CPUでGPUの結果を使うためには、GPUで作成したテクスチャをロックしてから読み込む手順が必要になります。
しかし、この方法は、本来のデータの流れとは違うものなので、処理は非常に低速です。
そこで、GPUで全ての処理を行う方法として、普通はメインメモリに置くパーティクルの座標をテクスチャにおき、 パーティクルの時間発展までGPUにやらせようという考えが自然に浮かんできます。
最近では、GDC2003での『Cloth Simulation』SIGGRAPH2003の『The GPU as Numerical Simulation Engine』などのそれらに関する論文が提出されています。
このプログラムを組んだときには、これらの論文は知らなかった時なので、 それらの方法とは違う方法になりますが、GPUを使ってパーティクルを飛ばしてみます。

今回のプログラムは、次のものです。
なお、ディスプレースメントマッピングと浮動小数点数バッファを使うので、 HALで実行できる環境はこの世には存在しません。

まぁ、いつものように適当にファイルが入っています。
APP WIZARD から出力されるフレームワークのファイルは紹介を省かせていただきます。

hlsl.fxシェーダの入ったエフェクトファイル
main.hアプリケーションのヘッダ
main.cppアプリケーションのソース

あと、実行ファイル、モデル及び、プロジェクトファイルが入っています。

■パーティクスを飛ばす方法

最近では、GPU は強力な超SIMD計算機だということが認識されるようになって来ました。
それは、テクスチャを配列と考えて、テクスチャを重ねたりピクセルシェーダを使うことによって、 配列全てに関して一度に同じ計算ができるということです。
この使い道で一番単純に考えられるのは、テクスチャのテクセルをパーティクルの位置座標とみなして、 非常に多くのパーティクルの計算を一度に行うことです。
今回は、まさにその方法を実践します。

パーティクルの変数をテクスチャに確保します(ここでは、particle map とでも名づけましょう)。
例えば、テクスチャの一番上の段に位置座標、2段目に速度、そして3段目に加速度を準備します。
また、別のパーティクルは横に並べます。今回は、64個のパーティクルを飛ばしました。

また、今回は、それぞれのテクセルについて、0.5の色値をベクトルの0にする。 符号化スケーリングの方法を取りました。
これは、ディスプレースメントマッピングのテクスチャが読み込まれた時の値の範囲が0~1の間にクランプされてしまったので、0.5を中心に持ってくるようにするためです。
将来的に、vs_3_0でテクスチャを読み込めるようになれば、浮動小数点数バッファの値そのものをベクトルとして使えるようになるでしょう。

位置、速度、加速度と並べておくと、何がいいかというと、それぞれのテクセルの下の段は、その変数の時間微分になっているので、縦にずらしたテクスチャを重ねるだけで、パーティクルの時間発展が可能になります。
つまり、時間発展の計算が次のようなポリゴンを1枚張る計算で済んでしまいます。

main.cpp
0207:     //-----------------------------------------------------
0208:     // 移動
0209:     //-----------------------------------------------------
0216:     m_pd3dDevice->SetFVF( D3DFVF_XYZRHW | D3DFVF_TEX2 | D3DFVF_TEXCOORDSIZE2(0) | D3DFVF_TEXCOORDSIZE2(1) );
0217:     m_pEffect->SetTechnique( m_hTechnique );
0218:     m_pEffect->Begin( NULL, 0 );
0219:     m_pEffect->Pass( 2 );
0220:     m_pEffect->SetTexture("ParticleMap", m_pParticleTex );
0221: 
0222:     typedef struct {FLOAT p[4]; FLOAT tu0, tv0; FLOAT tu1, tv1;} T2VERTEX;
0223:     T2VERTEX TVertex[4] = {
0224:         //         x         y  z rhw             tu1                     tv1                        tu2                     tv2
0225:         {{                0, 0, 0, 1}, 0+0.5f/PARTICLEMAP_WIDTH, 0.5f/PARTICLEMAP_HEIGHT, 0+0.5f/PARTICLEMAP_WIDTH, 1.5f/PARTICLEMAP_HEIGHT,},
0226:         {{PARTICLEMAP_WIDTH, 0, 0, 1}, 1+0.5f/PARTICLEMAP_WIDTH, 0.5f/PARTICLEMAP_HEIGHT, 1+0.5f/PARTICLEMAP_WIDTH, 1.5f/PARTICLEMAP_HEIGHT,},
0227:         {{                0, 2, 0, 1}, 0+0.5f/PARTICLEMAP_WIDTH, 2.5f/PARTICLEMAP_HEIGHT, 0+0.5f/PARTICLEMAP_WIDTH, 3.5f/PARTICLEMAP_HEIGHT,},
0228:         {{PARTICLEMAP_WIDTH, 2, 0, 1}, 1+0.5f/PARTICLEMAP_WIDTH, 2.5f/PARTICLEMAP_HEIGHT, 1+0.5f/PARTICLEMAP_WIDTH, 3.5f/PARTICLEMAP_HEIGHT,},
0229:     };
0230:     m_pd3dDevice->DrawPrimitiveUP( D3DPT_TRIANGLESTRIP, 2, TVertex, sizeof( T2VERTEX ) );

シェーダを使っているのは、ベクトルの0点を0にするために、少し計算が必要だったからで、基本的には、加算合成です。
ちなみに、つかっているシェーダは次のようになります。

hlsl.fx
0119: float4 ParticlePS(PARTICLE_VS_OUTPUT In) : COLOR
0120: {   
0121:     float4 Color;
0122:     
0123:     Color  =  tex2D( ParticleSamp, In.x )
0124:             + tex2D( ParticleSamp, In.dx )
0125:             - 0.502;// 0.5を中央にする。0.002は???
0126:     return Color;
0127: }

まぁ、実際には、0.5でなくて、0.502を引いてベクトルの0点を合わせています。 この0.002の違いは、よく分かりません。実際の動きを見て値を調整しました。 浮動小数点数バッファを使っているのに、どうしてでしょう?
ここでは、時間微分の変数をそのまま加えていますが、 このままではFPSに依存してしまうので、 気になる方は、定数レジスタに前のフレームからの時間差を入れておいて、 その時間差を掛けておいて足しこむと、より良くなると思います。

実際にパーティクルを動かすのは、現時点では他に方法が無いので、ディスプレースメントマッピングを使いました。
ディスプレースメントマッピングでは、HLSLが使えないので、じみちにアセンブラを書きます。
頂点データでは、読み込んだディスプレースメントマップの値を符号化スケーリングして頂点座標に足しこみます。
今回は、-1~1の間の箱の中に閉じ込めたので、符号化した値をそのまま使いましたが、 動ける範囲がこの範囲よりも大きい場合は、ディスレースメントマップの値を適当にスケーリングして使います。

hlsl.fx
0064: // -------------------------------------------------------------
0065: // パーティクルの描画
0066: // 
0067: //  c0 - c3 - 座標変換
0068: //  c12     - (0.0, 0.5, 1.0, 2.0)
0069: // -------------------------------------------------------------
0070: VertexShader DisplacementVS = asm
0071: {
0072:     vs_1_1
0073:     
0074:     dcl_position0 v0
0075:     dcl_normal0   v1
0076:     dcl_texcoord0 v2
0077:     dcl_sample0   v3
0078:     
0079:     def c12, 0.0f ,0.5f, 1.0f, 2.0f
0080:     
0081:     mad r0, v3, c12.w, -c12.z
0082:     add r0, v0, r0      // ディスプレースメントの量を頂点に追加
0083:     mov r0.w, v0.w      // w=1
0084:     m4x4 oPos, r0, c0   // スクリーン座標へ
0085:     
0086:     mov oT0, v2         // テクスチャ座標
0087: };

描画するときには、テッセレータをNパッチにしてから描画します。
図では「点」を表示しているように見えますが、普通にメッシュを表示しています。
大きくすることもできますが、遅くなるので・・・

main.cpp
0651:         //-------------------------------------------------------------------------
0652:         // ディスプレースメントマッピングを使ってパーティクルを描画する
0653:         //-------------------------------------------------------------------------
0654:         m_pEffect->SetTechnique( m_hTechnique );
0655:         m_pEffect->Begin( NULL, 0 );
0656:         m_pEffect->Pass( 0 );
0657: 
0658:         // 変換行列
0659:         m = m_mView * m_mProj;
0660:         m_pEffect->SetMatrix( m_hmWVP, &m );
0661:         // テクスチャ
0662:         m_pEffect->SetTexture("DecaleMap", m_pParticle->GetObj(0)->GetMesh()->m_pTextures[0] );
0663: 
0664:         m_pd3dDevice->SetNPatchMode(1); // ディスプレースメントマップを使用する
0665:         m_pd3dDevice->SetVertexDeclaration( m_pDecl );
0666:         m_pd3dDevice->SetTexture(D3DDMAPSAMPLER, m_pParticleTex);
0667:         for(DWORD i=0;i<PARTICLE_MAX;i++){
0668:             m_pParticle->GetObj(i)->GetMesh()->Render( m_pd3dDevice );
0669:         }
0670:         m_pd3dDevice->SetNPatchMode(0); // ディスプレースメントをやめる
0671:         m_pEffect->End();

なお、メッシュには、パーティクルごとに専用のものを用意しています。
違いは、ディスプレースメントマップを指定するテクスチャ座標です。
それぞれのメッシュの全ての頂点に関して、全て同じParticle map の位置座標を指定します。

main.cpp
0082: void CParticle::RestoreDeviceObjects(LPDIRECT3DDEVICE9 pd3dDevice)
0083: {
0084:     if( pMesh->m_pSysMemMesh ){
0085:         if( FAILED( pMesh->m_pSysMemMesh->CloneMesh(
0086:                         D3DXMESH_NPATCHES, decl,
0087:                         pd3dDevice, &pMesh->m_pLocalMesh ) ) )
0088:             return;
0089:         D3DXComputeNormals( pMesh->m_pLocalMesh, NULL );
0090:         
0091:         // ディスプレースメントマップの座標を指定する
0092:         MESH_VERTEX* pVertices;
0093:         pMesh->m_pLocalMesh->LockVertexBuffer( 0L, (LPVOID*)&pVertices );
0094:         for(DWORD i=0;i<pMesh->m_pLocalMesh->GetNumVertices();i++){
0095:             pVertices[i].du = (0.5f+(FLOAT)id)/PARTICLEMAP_WIDTH;
0096:             pVertices[i].dv = 0.5f/PARTICLEMAP_HEIGHT;
0097:         }
0098:         pMesh->m_pLocalMesh->UnlockVertexBuffer();
0099:     }

DirectX9からは、頂点データのマルチストリームができるので、 きちんと作ればディスプレースメントマップのデータだけを別にできると思います。

■初期化や値のリセット

あと、必ず必要になってくるのが初期化です。
パーティクスの位置の初期化などは、対応するテクセルにポリゴンを上塗りすることによって可能です。
今回は、位置と速度に関して同じような乱数を使って、テクセルの一つ一つを塗りつぶすことによって初期化しました。
初期値は、精度が落ちないようにピクセルシェーダの定数としてシェーダに渡しました。
このように1点1点づつ設定するならば、ポリゴンでなくて、ポイントリストを使ったほうが効率的かもしれません。

main.cpp
0140:     //-------------------------------------------------------------------------
0141:     // 位置、速度の初期化
0142:     //-------------------------------------------------------------------------
0143:     LVERTEX Vertex[4] = {
0144:         //        x                  y           z
0145:         {{                0,                  0,0.5,1},},
0146:         {{PARTICLEMAP_WIDTH,                  0,0.5,1},},
0147:         {{                0, PARTICLEMAP_HEIGHT,0.5,1},},
0148:         {{PARTICLEMAP_WIDTH, PARTICLEMAP_HEIGHT,0.5,1},},
0149:     };
0150: 
0151:     m_pEffect->SetTechnique( m_hTechnique );
0152:     m_pEffect->Begin( NULL, 0 );
0153:     m_pEffect->Pass( 1 );
0154:     for(i=0;i<PARTICLEMAP_WIDTH;i++){
0155:     for(j=0;j<2;j++){
0156:         v.x = 0.1f*(frand()-0.5f)+0.5f;
0157:         v.y = 0.1f*(frand()-0.5f)+0.5f;
0158:         v.z = 0.1f*(frand()-0.5f)+0.5f;
0159:         v.w = 1.0f;
0160: 
0161:         Vertex[0].p[0] = (float)i    ;
0162:         Vertex[1].p[0] = (float)(i+1);
0163:         Vertex[2].p[0] = (float)i    ;
0164:         Vertex[3].p[0] = (float)(i+1);
0165:         Vertex[0].p[1] = (float)j    ;
0166:         Vertex[1].p[1] = (float)j    ;
0167:         Vertex[2].p[1] = (float)(j+1);
0168:         Vertex[3].p[1] = (float)(j+1);
0169: 
0170:         m_pEffect->SetVector(m_hvVector, &v );
0171:         m_pd3dDevice->DrawPrimitiveUP( D3DPT_TRIANGLESTRIP, 2, Vertex, sizeof( LVERTEX ) );
0172:     }
0173:     }
0174:     m_pEffect->End();

ピクセルシェーダでは、単純に定数を出力します。

hlsl.fx
0110: float4 InitPS(INIT_VS_OUTPUT In) : COLOR
0111: {   
0112:     return vVector;
0113: }

他にも、データを上書きして設定している部分として、 毎フレーム、加速度を与える部分があります。
今回は、加速度が一定で作っているので、毎フレーム設定する必要は無いのですが、 一般的な場合を考えて、毎フレーム初期化しました。
この塗りつぶしは、1枚のポリゴンで全てのパーティクルの設定が可能なので、 GPUによるパイプラインの並列動作の効果を授受できます。

main.cpp
0259:     //-----------------------------------------------------
0260:     // 加速度の再設定
0261:     //-----------------------------------------------------
0262:     m_pd3dDevice->SetTextureStageState(0,D3DTSS_COLOROP,    D3DTOP_SELECTARG1);
0263:     m_pd3dDevice->SetTextureStageState(0,D3DTSS_COLORARG1,  D3DTA_DIFFUSE);
0264:     m_pd3dDevice->SetTextureStageState(1,D3DTSS_COLOROP,   D3DTOP_DISABLE);
0265: 
0266:     m_pd3dDevice->SetFVF( D3DFVF_XYZRHW | D3DFVF_DIFFUSE );
0267:     LVERTEX ClearVertex[4] = {
0268:         //       x          y z rhw   color
0269:         {{                0,2,0, 1}, 0x807c80,},
0270:         {{PARTICLEMAP_WIDTH,2,0, 1}, 0x807c80,},
0271:         {{                0,3,0, 1}, 0x807c80,},
0272:         {{PARTICLEMAP_WIDTH,3,0, 1}, 0x807c80,},
0273:     };
0274:     m_pd3dDevice->DrawPrimitiveUP( D3DPT_TRIANGLESTRIP, 2, ClearVertex, sizeof( LVERTEX ) );

ちなみに、y成分だけ塗りつぶすときの値が小さくなっています。
これは、下方向に重力加速度をつけるためのものです。

■当たり判定

パーティクルを飛ばすのはいいですが、自由気ままに飛ばすと、どこかに飛んでいってしまうので、 当たり判定をいれて、飛ばないようにします。
今回は、[-1,+1]の大きさの箱の中にパーティクルを閉じ込めました。
閉じ込めるために2つの処理をします。
1.箱の外にいたら速度ベクトルの向きを反転する
2.箱の外にいたら壁までパーティクルを押し戻す。
ちなみに、1と2の順番は入れ替えることができません。
入れ替えてしまうと、2の判定によって押し戻された位置で計算したときに、 「外にいた」と判定されなくなる可能性があるからです。
それぞれの過程は、テクスチャの位置と速度のそれぞれの部分にポリゴンを張ることによって処理されます。

main.cpp
0232:     //-----------------------------------------------------
0233:     // 跳ね返り
0234:     //-----------------------------------------------------
0235: 
0236:     // 速度調整
0237:     T2VERTEX TVertexV[4] = {
0238:         //         x         y  z rhw             tu1                     tv1                        tu2                     tv2
0239:         {{                0, 1, 0, 1}, 0+0.5f/PARTICLEMAP_WIDTH, 0.5f/PARTICLEMAP_HEIGHT, 0+0.5f/PARTICLEMAP_WIDTH, 1.5f/PARTICLEMAP_HEIGHT,},
0240:         {{PARTICLEMAP_WIDTH, 1, 0, 1}, 1+0.5f/PARTICLEMAP_WIDTH, 0.5f/PARTICLEMAP_HEIGHT, 1+0.5f/PARTICLEMAP_WIDTH, 1.5f/PARTICLEMAP_HEIGHT,},
0241:         {{                0, 2, 0, 1}, 0+0.5f/PARTICLEMAP_WIDTH, 0.5f/PARTICLEMAP_HEIGHT, 0+0.5f/PARTICLEMAP_WIDTH, 1.5f/PARTICLEMAP_HEIGHT,},
0242:         {{PARTICLEMAP_WIDTH, 2, 0, 1}, 1+0.5f/PARTICLEMAP_WIDTH, 0.5f/PARTICLEMAP_HEIGHT, 1+0.5f/PARTICLEMAP_WIDTH, 1.5f/PARTICLEMAP_HEIGHT,},
0243:     };
0244:     m_pEffect->Pass( 3 );
0245:     m_pd3dDevice->DrawPrimitiveUP( D3DPT_TRIANGLESTRIP, 2, TVertexV, sizeof( T2VERTEX ) );
0246:     // 位置調整
0247:     T2VERTEX TVertexX[4] = {
0248:         //         x         y  z rhw             tu1                     tv1                        tu2                     tv2
0249:         {{                0, 0, 0, 1}, 0+0.5f/PARTICLEMAP_WIDTH, 0.5f/PARTICLEMAP_HEIGHT, 0+0.5f/PARTICLEMAP_WIDTH, 1.5f/PARTICLEMAP_HEIGHT,},
0250:         {{PARTICLEMAP_WIDTH, 0, 0, 1}, 1+0.5f/PARTICLEMAP_WIDTH, 0.5f/PARTICLEMAP_HEIGHT, 1+0.5f/PARTICLEMAP_WIDTH, 1.5f/PARTICLEMAP_HEIGHT,},
0251:         {{                0, 1, 0, 1}, 0+0.5f/PARTICLEMAP_WIDTH, 0.5f/PARTICLEMAP_HEIGHT, 0+0.5f/PARTICLEMAP_WIDTH, 1.5f/PARTICLEMAP_HEIGHT,},
0252:         {{PARTICLEMAP_WIDTH, 1, 0, 1}, 1+0.5f/PARTICLEMAP_WIDTH, 0.5f/PARTICLEMAP_HEIGHT, 1+0.5f/PARTICLEMAP_WIDTH, 1.5f/PARTICLEMAP_HEIGHT,},
0253:     };
0254:     m_pEffect->Pass( 4 );
0255:     m_pd3dDevice->DrawPrimitiveUP( D3DPT_TRIANGLESTRIP, 2, TVertexX, sizeof( T2VERTEX ) );

ともに、テクスチャ座標の1つめに、パーティクルの位置座標、2つめに速度を指定して描画します。
それぞれのパスで指定されたシェーダの中身が当たり判定のみそです。
といってもそれほど複雑ではありません。
単純に成分ごとに外にあるかどうかを判定して、外に出ていたら、それなりの処理をするだけです。

1つめの速度の場合には、成分ごとにはみ出たら向きを変えます。
ちなみに、ただ向きを変えただけでは、この後の位置の移動とあいまって、 速さがどんどん速くなってしまうので、跳ね返り定数を入れて発散を押さえるようにしています。

main.cpp
0130: // -------------------------------------------------------------
0131: // 跳ね返り(速度)
0132: // -------------------------------------------------------------
0133: float4 ReflectVPS ( REFLECT_VS_OUTPUT In ) : COLOR0
0134: {
0135:     float4 x = 2.0f*tex2D( ParticleSamp, In.x )-1.0f;
0136:     float4 v = 2.0f*tex2D( ParticleSamp, In.v )-1.0f;
0137:     
0138:     if(x.x<-1.0f || +1.0f<x.x) v.x = -v.x;
0139:     if(x.y<-1.0f || +1.0f<x.y) v.y = -0.8f*v.y;
0140:     if(x.z<-1.0f || +1.0f<x.z) v.z = -v.z;
0141:     
0142:     return 0.5f*v + 0.5f;
0143: }

位置のほうも、はみ出ていたら、境界部分に位置をクランプするようにします。

main.cpp
0144: // -------------------------------------------------------------
0145: // 跳ね返り(位置)
0146: // -------------------------------------------------------------
0147: float4 ReflectXPS ( REFLECT_VS_OUTPUT In ) : COLOR0
0148: {
0149:     float4 x = 2.0f*tex2D( ParticleSamp, In.x )-1.0f;
0150:     float4 v = 2.0f*tex2D( ParticleSamp, In.v )-1.0f;
0151:     
0152:     if(x.x<-1.0f) x.x = -1.0f; else
0153:     if(+1.0f<x.x) x.x = +1.0f;  
0154:     if(x.y<-1.0f) x.y = -1.0f; else
0155:     if(+1.0f<x.y) x.y = +1.0f;  
0156:     if(x.z<-1.0f) x.z = -1.0f; else
0157:     if(+1.0f<x.z) x.z = +1.0f;  
0158:     
0159:     return 0.5f*x + 0.5f;
0160: }

今回は、当たり判定は箱の中にいる条件を使いましたが、 当たり判定シェーダの技術を応用すれば、より複雑な地形に対して当たり判定を行なうことができます。
まぁ、それはのちのち紹介していきます。

■最後に

ということで、GPUを使ったパーティクルの計算です。
重いので正当な評価はできませんが、それは、vs_3_0が使えるようになる時までとっておきましょう。
それにしても、OpenGLだと、このレベルのシェーダがリアルタイムに実行できるんですよね。
最新のAPIがOpenGLだというのは、変な時代になりました。





もどる

imagire@gmail.com