DirectXの話‎ > ‎

DirectXの話 第130回

Tessellation事始め 


前回の予告通り、今回はTessellationの事始め的なサンプルを書いてみました。
サンプル自体は極めて単純なものですので、難しいことはないと思います。
今回はDirectX11におけるTessellationの使い方、処理の流れ、各シェーダの役割等を自身の覚え書き代わりに記事にしています。

Tessellationはモザイク状配列とかそういう意味があるそうですが、CGの世界ではポリゴンを分割する技術として知られています。
上の画面では三角形と四角形が複数の三角形で構成されていますが、元になったデータは3頂点の三角形と4頂点の四角形です。
上図のようなポリゴン分割を行ってくれるのがTessellatorです。
下図はDirectX11のTessellatorを使用する場合のパイプラインです。


Vertex Shaderから開始するのは通常の描画と変わりません。
その後、Geometry Shader、もしくはPixel Shaderに入るのが一般的ですが、この間に3つのステージが追加されます。
Hull ShaderTessellatorDomain Shaderです。
これらのステージはそれぞれ役割が違いますが、Tessellatorを使用する場合は必ずこの3つのステージを使用しなければなりません。
なお、上図ではDomain ShaderのあとにGeometry Shaderが来ていますが、Geometry Shaderを無視してPixel Shaderに進むことももちろん可能です。

それぞれのステージは色々な説明がされていますが、たいていは

 ・Hull Shaderは分割方法やパラメータを定義する
 ・Tessellatorはポリゴンの分割を行う
 ・Domain Shaderは分割されて生成された頂点に対して処理を行う

という説明がされているかと思います。
大きく間違ってはいませんが、これだけではよくわからないと思ったのは多分私だけではないでしょう。
実装部分を見ながらの説明の方がわかりやすいと思いますので、サンプルのシェーダコードを交えて簡単に説明したいと思います。

まずはHull Shaderです
Hull Shaderはプログラマブルで、シェーダコードを記述する必要があります。
他のシェーダとの違いは、2つの関数で構成されるという点でしょう。
Tessellatorを使用する場合、DirectXに渡されるトポロジーは三角形やラインではなく、パッチという単位で渡されることになります。
パッチはIASetPrimitiveTopology() メソッドに対してD3D11_PRIMITIVE_TOPOLOGY_3_CONTROL_POINT_PATCHLISTなどのパラメータを指定することで使用できるようになります。
上述のパラメータはコントロールポイントを3つ使用するパッチということになりますが、1~32までのコントロールポイントを1つのパッチに対して割り当てられます。
Hull Shaderはパッチごとに適用される関数と、コントロールポイントごとに適用される関数によって成り立ちます。
以下はHull Shaderの簡単な記述方法です。

struct TriConstant
{
    float tessFactor[3] : SV_TessFactor;
    float insideFactor  : SV_InsideTessFactor;
};

TriConstant RenderTriConstantHS( InputPatch<InputVS, 3> iPatch )
{
    TriConstant outConst;

    float3 retf;
    float  ritf, uitf;
    ProcessTriTessFactorsAvg( g_TessFactor.xyz, g_InsideFactor.x, retf, ritf, uitf );

    outConst.tessFactor[0] = retf.x;
    outConst.tessFactor[1] = retf.y;
    outConst.tessFactor[2] = retf.z;
    outConst.insideFactor = ritf;

    return outConst;


[domain("tri")]
[partitioning("integer")]
[outputtopology("triangle_cw")]
[outputcontrolpoints(3)]
[patchconstantfunc("RenderTriConstantHS")]
InputVS RenderTriIntegerHS( InputPatch<InputVS, 3> iPatch,  uint pointID : SV_OutputControlPointID )
{
    return iPatch[pointID];
}

RenderTriConstantHS() がパッチ単位で発行されるPatchConstantFunctionで、RenderTriIntegerHS() がコントロールポイントごとに発行されます。
シェーダをコンパイルする場合はコントロールポイントごとのシェーダ名でコンパイルします。
パッチごとのシェーダはコントロールポイントごとのシェーダの属性patchconstatnfuncで指定します。

PatchConstantFunctionの役割はパッチごとの分割に関するパラメータを生成することです。
これは SV_TessFactor SV_InsideTessFactor を指定したパラメータに格納します。
SV_TessFactor はパッチの各エッジに対する分割数です。1.0を指定すると分割が行われません。
SV_InsideTessFactor はパッチの内部の分割数です。
これらの配列要素数は分割するプリミティブタイプによって変化します。
例えば、SV_TessFactorは三角形の場合は3、四角形の場合は4となります。
今回のサンプルでは ProcessTriTessFactorsAvg() 関数等を使用してこれらのパラメータを整形していますが、必ずこの関数を通さなければならないというわけでもありません。
これらの関数を通す場合、第2引数には0.0~1.0を与えなければなりませんが、SV_InsideTessFactor には分割数を入れることになります。

次にコントロールポイントごとに呼び出される関数ですが、こちらにはいくつかの属性を指定する必要があります。
それぞれの属性は以下の表のような意味を持っています。

domain分割するプリミティブタイプです。tri, quad, isoline の3つから選択でき、それぞれが三角形、四角形、線分となっています。
partitioning分割方法です。integer, fractional_eve, fractional_odd, pow2から選択できます。
outputtopology出力された頂点によって形成されるトポロジーです。point, line, triangle_ccw, triangle_cwから選択できます。
outputcontrolpoints出力されるコントロールポイントの数です。入力されたコントロールポイントと同じ数値にしなければならない訳ではありません。
patchconstantfuncパッチごとに呼び出される関数を指定します。

[outputcontrolpoints]属性は注意が必要です。
Hull Shaderで出力されるコントロールポイントの数はこの属性で指定されますが、入力されるコントロールポイントの数はこの数値と一致しているとは限りません。
Hull Shaderの引数としてInputPatch<> というものがありますが、これがパッチ1つに対する入力されたコントロールポイントの配列となります。
InputPatch<> は入力コントロールポイントの型と数が指定されます。数はIASetPrimitiveTopology() メソッドで指定したコントロールポイント数と同一である必要があります。
しかし、コントロールポイントごとに発行されるHull Shaderは出力コントロールポイントの数だけ1パッチごとに発行されます。
つまり、入力コントロールポイントが1つだったとしても、出力コントロールポイントに3が指定されていれば3回の関数呼び出しが行われるわけです。
コントロールポイントの入出力数を変えることで何が出来るのか?
…何が出来るんでしょう?
パッと思いつく点ではモーフィングとか? でも、コントロールポイントを増やす理由にならないしなぁ…
まあ、なんか私が知らない技術とかで使われるんでしょう、きっと。

Hull Shaderの解説が長くなりましたが、次はTessellatorステージです
ここはプログラマブルではなく、Hull Shaderの属性と、PatchConstantFunctionで出力したTessFactor等を用いてポリゴン分割を行います。
Hull Shaderでパラメータを指定する以外にTessellatorの結果を変更することは出来ません。

最後にDomain Shaderです
Domain ShaderにはHull Shaderから出力されたコントロールポイントすべてとTessellatorによってポリゴン分割された時にパラメータが入力されます。

[domain("tri")]
InputPS RenderTriDS( TriConstant inConst, float3 domLoc : SV_DomainLocation, const OutputPatch<InputVS, 3> oPatch )
{
    InputPS outVert;

    outVert.pos.xyz = oPatch[0].pos * domLoc.z + oPatch[1].pos * domLoc.x + oPatch[2].pos * domLoc.y;
    outVert.pos.w = 1.0;

    return outVert;
}

OutputPatch<>の配列要素数はHull Shaderのoutputcontrolpointsの数です。
SV_DomainLocationで指定されているパラメータがTessellatorが出力するパラメータです。
Domain Shaderは分割によって生成された頂点数分だけ呼び出されますが、頂点座標が直接入力されるわけではないことに注意してください。
SV_DomainLocationは頂点座標を求めるためのパラメータでしかなく、Domain Shader内で必要な頂点を生成しなければなりません。
上述のDomain ShaderはSV_DomainLocationを利用して三角形内部に頂点を生成しています。
もちろん、全く無関係な場所に頂点を生成することも可能ですが、プリミティブの表面はインデックスによって生成されるので、生成方法によってはポリゴンが破綻するなどの問題が発生します。

また、Geometry Shaderを呼び出さない場合はスクリーン空間変換はここで行う必要があります。
他にも、ディスプレイスメントマップを使用する場合はここでテクスチャを参照し、適切な高さを与えてやる必要があります。
生成するのは頂点だけではなく、法線、UV座標、タンジェントなども必要なら生成しなければいけません。
これらの生成にもSV_DomainLocationが使用されます。
今回はそれらの生成は行っていませんが、次回以降で何かやりたいとは思っています。

さて、サンプルは下記からDLしてください。

Download : Sample120.zip

メニューの[One Value Use]がtrueの場合はTessFactor, InsideFactorのパラメータは0番しか使用しません。
つまり、すべてのFactorが0番の数値で統一されることになります。
パラメータの違いによってどのように分割方法が変わるのかはサンプルを動かして確認してみてください。

このサンプルのようなテストプログラムがあると、どのパラメータでどのように分割が変化するかわかりやすくなると思います。
Tessellatorを使用したCGに関してはデザイナさんもプログラマもまだ慣れてはいません。
しかし、今後は必ず必要な技術になってくるでしょう。
その際にどう分割すると結果が良くなるのか、パラメータの変化による分割の違いはどうなっているのかを確認できると学習はしやすくなるんじゃないでしょうか?
まあ、最終的には感覚で処理されてしまうかもしれませんが。

あと、注意点として1点だけ。
現段階ではHull ShaderとDomain Shaderを使用する場面はそれほど多くはないと思います。
そのため、今まで作成したプログラムではHull ShaderとDomain Shaderを明示的に使用しないようには設定していないと思います。
ですので、Hull ShaderやDomain Shaderを使用した直後に使用しない設定にしておくと良いかと思います。
そんな単純な不具合の調査に30分かけてしまった人間からの忠告でした。
Comments