いくつか質問があります。長文になってしまい申し訳ありません。お時間のあるときに1つずつでも教えていただければ幸いです。
0.前提として、私は大量のパーティクルの描画をできるだけ自由に行うことが大きな目標としており、
最近まではDrawPolygonIndexed3Dで描画を行なっており、リリースビルドしたもので大体6000〜7000枚程度までの満足の行く操作を含めたビルボードを60fpsで描画出来ていました。
しかし行列演算が多く、シェーダを書いてGPUで演算すれば少なくとも10万枚程度は描画出来ると考え、開発を進めてきました。
PCの簡単なスペックはCPU:2500K GPU:7750 メモリ:12GB OS:win7_64bitです。
質問に関わるコードを貼っておきます。下の質問からお読みになって下さい。
for( i = 0 ; i < MAX_PARTICLE_NUMBER ; i ++ )
{
//指向軸ベクトルの取得
Direct =VNorm( Particle[ i ].V); //Particle[ i ].Vはそのパーティクルの速度ベクトルです
// 4頂点分のデータをセット
Vertex[ 4*i+0 ].pos = VGet(Particle[ i ].size_pos_1_x,Particle[ i ].size_pos_1_y,0); //※1 後述します
Vertex[ 4*i+0 ].norm = VGet(0,0,-1);
Vertex[ 4*i+0 ].dif = GetColorU8(Col_r_Now,Col_g_Now,Col_b_Now,Col_a_Now) ;
Vertex[ 4*i+0 ].spc = GetColorU8( 0, 0, 0, 0 ) ;
Vertex[ 4*i+0 ].u = 0.0f ;
Vertex[ 4*i+0 ].v = 0.0f ;
Vertex[ 4*i+0 ].su = 0.0f ;
Vertex[ 4*i+0 ].sv = 0.0f ;
Vertex[ 4*i+1 ].pos = VGet(Particle[ i ].size_pos_2_x,Particle[ i ].size_pos_2_y,0);
Vertex[ 4*i+1 ].norm = VGet(0,0,-1));
Vertex[ 4*i+1 ].dif = GetColorU8(Col_r_Now,Col_g_Now,Col_b_Now,Col_a_Now) ;
Vertex[ 4*i+1 ].spc = GetColorU8( 0, 0, 0, 0 ) ;
Vertex[ 4*i+1 ].u = 0.0f ;
Vertex[ 4*i+1 ].v = 1.0f ;
Vertex[ 4*i+1 ].su = 0.0f ;
Vertex[ 4*i+1 ].sv = 0.0f ;
Vertex[ 4*i+2 ].pos = VGet(Particle[ i ].size_pos_3_x,Particle[ i ].size_pos_3_y,0);
Vertex[ 4*i+2 ].norm = VGet(0,0,-1);
Vertex[ 4*i+2 ].dif = GetColorU8(Col_r_Now,Col_g_Now,Col_b_Now,Col_a_Now) ;
Vertex[ 4*i+2 ].spc = GetColorU8( 0, 0, 0, 0 ) ;
Vertex[ 4*i+2 ].u = 1.0f ;
Vertex[ 4*i+2 ].v = 0.0f ;
Vertex[ 4*i+2 ].su = 0.0f ;
Vertex[ 4*i+2 ].sv = 0.0f ;
Vertex[ 4*i+3 ].pos = VGet(Particle[ i ].size_pos_4_x,Particle[ i ].size_pos_4_y,0);
Vertex[ 4*i+3 ].norm = VGet(0,0,-1);
Vertex[ 4*i+3 ].dif = GetColorU8(Col_r_Now,Col_g_Now,Col_b_Now,Col_a_Now) ;
Vertex[ 4*i+3 ].spc = GetColorU8( 0, 0, 0, 0 ) ;
Vertex[ 4*i+3 ].u = 1.0f ;
Vertex[ 4*i+3 ].v = 1.0f ;
Vertex[ 4*i+3 ].su = 0.0f ;
Vertex[ 4*i+3 ].sv = 0.0f ;
// 2ポリゴン分のインデックスデータをセット
Index[ 6*i+0 ] = 4*i+0 ;
Index[ 6*i+1 ] = 4*i+1 ;
Index[ 6*i+2 ] = 4*i+2 ;
Index[ 6*i+3 ] = 4*i+3 ;
Index[ 6*i+4 ] = 4*i+2 ;
Index[ 6*i+5 ] = 4*i+1 ;
// シェーダ定数を設定(レジスタへ渡す)
FLOAT4 f4array[ 6 ] ;
f4array[ 0 ].x = Direct.x ;
f4array[ 0 ].y = Direct.y ;
f4array[ 0 ].z = Direct.z ;
f4array[ 0 ].w = 0 ;
f4array[ 1 ].x = Particle[ i ].L_Axes.x ; //前フレームでの外積軸です。シェーダでローカル軸を求める際に使います。
f4array[ 1 ].y = Particle[ i ].L_Axes.y ;
f4array[ 1 ].z = Particle[ i ].L_Axes.z ;
f4array[ 1 ].w = 0 ;
f4array[ 2 ].x = Particle[ i ].pos.x ; //パーティクルの座標
f4array[ 2 ].y = Particle[ i ].pos.y ;
f4array[ 2 ].z = Particle[ i ].pos.z ;
f4array[ 2 ].w = 0 ;
f4array[ 3 ].x = Particle[ i ].rotX ; //パーティクルのローカル各軸の自転角度
f4array[ 3 ].y = Particle[ i ].rotY ;
f4array[ 3 ].z = Particle[ i ].rotZ ;
f4array[ 3 ].w = 0 ;
f4array[ 4 ].x = sizeX_Now ; //パーティクルの大きさ
f4array[ 4 ].y = sizeY_Now ; //時間に応じた展開などのためにこれの上でそのフレームでのサイズを算出しています
f4array[ 4 ].z = sizeZ_Now ; //今のところビルボードしか考えていないので事実上Z軸は無意味です
f4array[ 4 ].w = 0 ;
f4array[ 5 ].x = cam[ sys.Control_No ].Eye.x ; //操作機体のカメラ座標
f4array[ 5 ].y = cam[ sys.Control_No ].Eye.y ;
f4array[ 5 ].z = cam[ sys.Control_No ].Eye.z ;
f4array[ 5 ].w = 0 ;
SetVSConstFArray( 43, f4array, 5 ) ;
//※1 size_pos〜はパーティクルの座標からローカル軸に沿って、サイズの何倍移動した点かを決定するための値です
//基本的にVertex[0]には(0.5 , 0.5 , 0)、Vertex[1]には(-0.5 , 0.5 , 0)、Vertex[2]には(0.5 , -0.5 , 0)、Vertex[3]には(-0.5 , -0.5 , 0)となっています
//そのパーティクルの要望に応じてシェーダを選択
if()
{
// 使用する頂点シェーダーのセット
SetUse VertexShader( Shader.Vsfor_Particle1 ) ;
}
//そのパーティクルの要望に応じてシェーダを選択
if()
{
// 使用するピクセルシェーダーをセット
SetUsePixelShader( Shader.Psfor_Particle1 ) ;
}
// 使用するテクスチャを0番にセット
SetUseTextureToShader( 0, Particle[ i ].GrHandle ) ;
// 2ポリゴンのシェーダ使った描画
DrawPolygonIndexed3DToShader( Vertex, 4*MAX_PARTICLE_NUMBER, Index+6*i, 2 ) ;
}
//頂点シェーダ
//簡単に書きます。
// C++ 側で設定する定数の定義
float4x4 cfViewMatrix : register( c6 ) ; // ワールド座標をビュー座標に変換する行列の転置行列
float4x4 cfProjectionMatrix : register( c2 ) ; // ビュー座標を射影座標に変換する行列の転置行列
float3 cfDirect : register( c43 ); // 指向軸
float3 L_Axes : register( c44 ); // 前フレームでの外積軸
float4 P_pos : register( c45 ); // パーティクルの座標
float4 P_Rot : register( c46 ); // パーティクルのx,y,z軸回転角
float4 P_Size : register( c47 ); // パーティクルのx,y,z軸方向の展開中の今フレームのサイズ。
float4 Cam_pos : register( c48 ); // カメラの座標
//ローカル軸の取得
Local_Axes_Y = 先ほどのDirectベクトルと(0,1,0)ベクトルの外積
Local_Axes_Z = DirectベクトルとLocal_Axes_Yの外積
if(Local_Axes_Yが零ベクトル)
{前フレームの外積軸から求めます。それでも零ベクトルのときは(1,0,0)ベクトルとの外積から求めます。}
Size_X_tmp = Local_Axes_X *VSInput.Position.x *P_Size.x;
Size_Y_tmp = Local_Axes_Y *VSInput.Position.y *P_Size.y;
lWorldPosition = Size_X_tmp+ Size_Y_tmp +P_pos;
// 頂点座標をビュー空間の座標に変換する
lViewPosition = mul( lWorldPosition, cfViewMatrix ) ;
// ビュー空間の座標を射影空間の座標に変換する
VSOutput.ProjectionPosition = mul( lViewPosition, cfProjectionMatrix ) ;
// テクスチャ座標はそのまま代入
VSOutput.TextureCoord0 = VSInput.TextureCoord0;
// 頂点カラーはそのまま代入
VSOutput.DiffuseColor = VSInput.DiffuseColor ;
// 関数の戻り値がピクセルシェーダーに渡される
return VSOutput ;
//ピクセルシェーダは頂点シェーダのテスト中なのでとりあえず
//出力する色はテクスチャの色と C++ で設定した値とディフューズカラーを乗算したもの を出力しています。
質問
1.ソースコードについて
大まかな流れはパーティクルの座標一点で運動(別ソース)させ、描画の際にのみローカル軸に沿って四点に広げるといった形です。
任意軸回転などの演算が重なっても描画の際のローカル軸のみで済む分、演算量が少ないと思うのでこのような書き方をしていますが、
これをコンパイルして実行してもうまく描画されません。ワールド座標の原点からpos方向に非常に細長いビルボードが描画されます。
さらにVertex[].posを0.5と-0.5以外の数字(1と0や2と1で同じ表現)では描画すらされません。
しかしシェーダの演算をVertex[].posの前に行い、直接Vertex[].posに入れてやると想像通りの結果が描画されます。
また、ローカル軸を無視して lWorldPosition = VSInput.Position *1000 + P_pos としてもきちんと描画されます。(理想通りではないですが)
シェーダでの演算の次元はきちんと合うようにしました。正直手詰まりです。おかしなところがあればご指摘お願い致します。
2.シェーダの値の流れについて
上の質問と重なりますが、頂点シェーダのVertex[].posは4頂点の値を1頂点ずつ(VSInputが行列ではなくベクトル型なので)シェーダの処理をし、
最後にIndexと描画ポリゴン数から4頂点を関連付けていると考えているのですが、間違っているでしょうか?(Yes or Noで結構です)
シェーダの例を見ているとローカル座標から4頂点共に同じ回転行列などをかけて、ワールド変換、ビュー空間、射影空間と変換しているものばかりなので
上の書き方はすごく奇特に見え、初心者なのでなにか特別な扱いがなされているのではないかと心配です。
3.DrawPolygonIndexed3DToShaderの引数について
以前 ttp://hpcgi2.nifty.com/natupaji/bbs/patio.cgi?mode=view&no=2764 こちらでDrawPolygonIndexed3Dの質問をさせていただいたときから
DrawPolygonIndexed3D( Vertex, 4*MAX_PARTICLE_NUMBER, Index+6*i, 2, Particle[ i ].GrHandle, TRUE ) ; としてきていましたが、
毎パーティクルごとに呼ぶなら 描画する三角形ポリゴンの数が2なので 第二引数のint VertexNum:使用する頂点の数 は4にはならないのでしょうか?(Yes or Noで結構です)
4.DrawPolygonIndexed3DToShaderの呼び出し回数について
現在一つのビルボードを描画するたびに呼び出しているので、毎フレーム数千回呼び出していることになりますが、
テクスチャが同じものは一回の呼び出しで一挙に描画するほうがやはり速いでしょうか。また、その差はかなり大きいですか?(それぞれYes or Noで結構です)
5.描画関数の速度について
今まで、DrawBillboard3DやDrawPolygonIndexed3D、DrawPolygonIndexed3DToShaderを使って来ましたが、
どうもDrawBillboard3Dが最も早かった印象です。冒頭に戻りますが、DrawPolygonIndexed3DToShaderを用いて、自分の出来る限り演算を省いたシェーダを書いてみましたが
3000〜4000枚でも画面の大部分を覆うと簡単に20〜30fpsまで落ちてしまい、CPUでDrawPolygonIndexed3Dを用いて描画した方が圧倒的に速かったです。
内部処理がよくわからないので教えていただきたいのですが、適切に扱えたならばDrawPolygonIndexed3DToShaderを用いた方が他の描画関数と同じ処理をしたとき、速いといえるでしょうか?
また、シェーダを使っていなかった時でもGPUの使用率が上がることがありましたが、これは内部で何らかの演算に使われているのでしょうか?それともただの動画視聴のようにDXlibとは無関係な部分での負荷でしょうか?
(それぞれYes or Noで結構です)
6.レジスタへの値の渡しについて
こちらもパーティクルごとにそれぞれの座標などを渡しているので毎フレーム数千回渡していることになりますが、これは非常識でしょうか?
とにかくパーティクル関係の演算をGPUに任せることで高速化を、と考えているのですが余計でしょうか。
MMDのMMEではif分の混ざった、行列演算も多いfxファイルのエフェクトが私の環境では50万パーティクルまではほぼ60fpsを保っていたため、(70万程度でGPU100%、処理落ち。CPUは10%以下)
一年近くの間、シェーダを随分当てにして開発を行っていました。
最適化など、様々な問題があると思われますが、それでも5万以上程度のパーティクルは扱ってみたいのです。
こういったことをするにはDXライブラリでは苦しいものがあるでしょうか。
また、あまり調べていませんがGPGPUのCUDAなどを導入することは助けになりそうでしょうか。
それとも私が未熟なだけで、シェーダのテクニックなどを除けばDXライブラリで充分大量のパーティクルを描画することぐらいは出来るでしょうか。
大変長くなり、誠に恐縮ですがお時間のあるときにでも教えていただけたらと思います。よろしくお願いします。