ホーム < ゲームつくろー! < プログラマブルシェーダ編
頂点シェーダ編
その1 多分必須!頂点変換の基本
プログラマブルシェーダでレンダリングをしようとした時に、まず真っ先に飛び込んでくるのが「頂点シェーダ」です。HLSLによる頂点シェーダには次のような情報が飛び込んできます:
頂点シェーダ入力セマンティクス セマンティクス 意味 POSITION ローカル空間の頂点の位置座標 BLENDWEIGHT ブレンディングの重み BLENDINDICES ブレンドのインデックス NORMAL 法線ベクトル PSIZE ポイントサイズ DIFFUSE ディフューズ色 SPECULAR スペキュラ色 TEXCOORD# テクスチャ座標 TANGENT 接線 BINORMAL 従法線 TESSFACTOR テセレーション係数
この中で多分最も大切なのは描画対象ポリゴンを構成する頂点のローカル座標位置であるPOSITIONセマンティクスです。固定機能を使うと何の意識も無く変換されるローカル座標内頂点ですが、頂点シェーダを自前で使う場合は頂点位置の変換も自前で実装する必要があります。しかも、頂点シェーダは少なくともPOSITIONセマンティクスを必ず出力しなければなりません。頂点変換は多分必須の実装なんです。
そこで、この章では頂点シェーダの基本実装である「頂点変換」を見ていくことにします。
@ ワールド変換・ビュー変換・射影変換行列をグローバルに
HLSLによる頂点シェーダプログラムはC言語と殆ど一緒のプログラムを組む事ができます。ただ、実際は外部から値を与えてもらって動くプログラムなので、値を受けるグローバル変数が必要になります。
頂点シェーダに入ってきたローカル座標は、最終的に「射影空間」に置くと出力可能な状態になります。これが頂点シェーダの一番の目的でもあります。このためには、頂点シェーダプログラムに「ワールド変換行列」「ビュー変換行列」「射影変換行列」を定義しておく必要があります。もちろんグローバル変数としてです。
変数の宣言部分を抜き出すと次のような記述になります:
行列変数宣言 float4x4 matWorld : WORLD;
float4x4 matView : VIEW;
float4x4 matProj : PROJECTION;
float4x4というのは4×4の行列を表す型でシェーダプログラムに最初からある組み込み型です。次に変数名を定義します。これは適当な名前で十分ですが、ここでは上の名前に統一します。その次にある「:」以下は「セマンティクス」というものでして、実は付けなくても構いません。でも、付けた方が変数の意味を理解しやすくなったり、シェーダを使用するプログラムからのアクセスが容易になったりします。本質ではありません。以後はここに適切な行列がすでに入っていると言う前提で話を進めます。
A 頂点シェーダ内での行列掛け算
同じ事を繰り返しており恐縮ですが、頂点シェーダには「ポリゴンを構成する頂点のローカル座標」が入ってきます。ローカル座標を射影空間に持っていくには、@で定義した行列を掛け算していきます。シェーダプログラム内でベクトルや行列の掛け算をするのは「mul」という組み込み関数です。この関数は非常にマルチな掛け算ができますが、ここでは次のようにコードを書けば十分です:
頂点変換をする頂点シェーダプログラム float4 BasicTrans_VS( float4 Pos : POSITION ) : POSITION
{
float4 Out; // 出力頂点座標
Out = mul( Pos, matWorld ); // ワールド変換
Out = mul( Out, matView ); // ビュー変換
Out = mul( Out, matProj ); // 射影変換
return Out;
}
mul関数は左の変数を右の変数にこの並びで掛け算します。行列演算は掛け算順序が大切なのでこれは注意したいポイントです。プログラムから自明なのですが、最初にローカル座標にワールド変換行列を掛けてワールド空間に変換します。この出力は4×1の行ベクトルになるため、Outは「float4」という行ベクトル型にしています。ワールドにある頂点に次にビュー変換行列を掛けて、さらに射影変換行列を掛け算すれば、頂点は射影空間に置かれる事になります。最後にそれをシェーダの出力としてreturnすれば完了です。
「3回も行列演算するくらいなら、ワールドビュー射影変換を外部から与えて1回に抑えた方が速いのでは?」と思われるかもしれませんが、そのためには外部(CPU)で行列を用意する必要があります。行列演算能力はGPUの方が多分長けていますので、実は上の演算の方がきっと効率が良いはずです(環境依存が高いために必ずとは言えません)。
B 頂点ブレンドが本番です
Aはいわゆる固定メッシュの描画を満たしますが、スキンメッシュの描画時にこの頂点シェーダを通すと、全くおかしな描画になります。それは「頂点ブレンド」を行っていないためです。固定機能はこれも自動化してくれていたのですが、頂点シェーダではそれも実装しなければいけません。この章の本番は実はこちらです。
頂点ブレンドは1つの頂点に複数のワールド変換行列を掛け算する事で柔らかい表皮のようにポリゴンメッシュを動かす方法の事です。これについてはDirectX技術編その27「アニメーションの根っこ:スキンメッシュアニメーション(ボーン操作)」をご覧下さい。ここではまず、頂点ブレンドの一般式を示します:
細かいので背景を白にしています
Pafterというのがブレンド後の頂点のワールド変換位置、Plocalが変換前のローカル位置、Numは合成する行列の数、matWorldiというのがi番目に作用するワールド変換行列、そしてWeightiがその行列が頂点に与える重み(0<=Weight<=1)です。最初のΣの中は、ワールド変換行列と重みを掛け算して全部足しています。ただし、最後の1つだけは除きます(Num-1に注目)。下段にある最後の1つのワールド行列には、それまで使用した重みの残りを掛け算します。そうしてブレンドした結果「1つの合成ワールド変換行列(matCombWorld)」が作成されます。
この式はNumが2つ以上を前提としています。Numが1つというのは、頂点ブレンドをしないいわゆる普通のワールド変換です。その場合下の足し算部分が0となります。これを条件文無しにプログラム化するのがポイントです。
この式に忠実に従ったコードを書くと、頂点ブレンドをサポートする頂点シェーダとなります:
頂点ブレンドをする頂点シェーダプログラム float4x4 matWorld[4] : WORLD; // ワールド変換行列配列
float4x4 matView : VIEW; // ビュー変換行列
float4x4 matProj : PROJECTION; // 射影変換行列
int iBlendNum; // ブレンドする配列の数
float4 BasicTrans_VS( float4 Pos : POSITION, float4 W : BLENDWEIGHT ) : POSITION
{
float4 P_after; // 出力頂点座標
float Weight[4] = (float[4])W; // 重みをfloatに分割します
float LastBlendWeight = 0.0f; // 最後の行列に掛けられる重み
float4x4 matCombWorld = 0.0f; // 合成ワールド変換行列
// ワールド変換行列をブレンド
for(int i=0; i<iBlendNum-1; i++)
{
LastBlendWeight += Weight[i]; // 最後の重みをここで計算しておく
matCombWorld += matWorld[i] * Weight[i];
}
// 最後の重みを足し算
matCombWorld += matWorld[iBlendNum-1] * (1.0f-LastBlendWeight);
P_after = mul( Pos, matCombWorld ); // ワールド変換
P_after = mul( P_after, matView ); // ビュー変換
P_after = mul( P_after, matProj ); // 射影変換
return P_after;
}
まず、頂点シェーダの入力入力セマンティクスにBLENDWEIGHTを追加して頂点に設定されている重みを取得します。これは1×4の行ベクトルとして入力されます。プログラム内でそれをまず4つのfloat型配列に分割します。後は先の式に従った計算を忠実に実装しているだけです。iBlendNumが1の時はforループは回りません。すると下の最後の重み部分の足し算だけが実行されます。LastBlendWeightはループが回れば値が大きくなっていく事に注目してください。
頂点の座標変換はきっと殆どの頂点シェーダで実装しなければならないと思います。特に頂点ブレンドのサポートは3Dスキンバリバリのゲームには必須です。基本的な事ですが重要です。