2022年8月18日木曜日

Tesselation(Hull Shader & Domain Shader)

3Dのプログラムを始める時、最初に説明されているのがグラフィックスパイプラインで、こんなフローを見ることになる。

Graphics Pipeline

最初に扱うのも今まで扱ってきたのもVSとPSのみ。VSだけっていうこともあった。
他のステージはよくわからないけどとりあえずそんなのもあるんだ程度で、もしかするとずっと使わないかもしれないと思ってた。

シェーダ以外の灰色の枠に関しては関与できず、パラメータを渡すのみ。
InputAssemblerに関しては、IAから始まる関数IASetPrimitiveTopology、IASetVertexBuffers、IASetIndexBufferを通して頂点バッファ、インデックスバッファを渡している。

今回の話題はTesselatoerの部分。

テッセレータ


頂点のデータをこのテッセレータが細かくしてくれるらしい。
どういうふうに細かくするかを決めるのがHull Shaderで、Hull Shaderの出力結果を使ってテッセレータが細かくする方針を立ててくれて、Domain Shaderで実際に決定する流れみたい。

だからテッセレータを使う場合はHSとDSはセットで用意する必要がある。
HSのみセットして実行すると怒られる。

D3D12 ERROR: ID3D12Device::CreateGraphicsPipelineState: When using tesselation, both the Hull Shader and Domain Shader must be set.  Otherwise, both must be NULL. [ STATE_CREATION ERROR #667: CREATEGRAPHICSPIPELINESTATE_HS_XOR_DS_MISMATCH]


Hull Shader


ハルシェーダは2つに分かれる。
1つは頂点データを設定するメイン関数。
もう1つは、分割の細かさを設定するパッチ定数関数。
頂点バッファをパッチという単位にして、1パッチ毎に呼ばれるのが定数関数で、パッチ&頂点単位に呼ばれるのがメイン関数。

メイン関数定義


	[domain("quad")]
	[partitioning("integer")]
	[outputtopology("triangle_ccw")]
	[outputcontrolpoints(4)]
	[patchconstantfunc("CHSMain")]
	HSOutput HSMain( InputPatch<VSOutput, 4> IP, uint CP : SV_OutputControlPointID, uint PatchID : SV_PrimitiveID )
    {
      	HSOutput Out ;
		Out.Pos = IP[CP].Pos ;
		Out.UV = IP[CP].UV ;
      	return Out ;
    }
メイン関数の先頭で色々な設定をする。

domain

domainに設定できる値は、quad(四角形)、tri(三角形)、isoline(線分)の3つ。
この値が決まると、outputcontrolpointsと、関数の引数InputPachの第二引数と、IASetPrimitiveTopologyの引数が決まるっぽい。
"quad"の場合は4、TopologyはD3D_PRIMITIVE_TOPOLOGY_4_CONTROL_POINT_PATCHLIST。
"tri"の場合は3、TopologyはD3D_PRIMITIVE_TOPOLOGY_3_CONTROL_POINT_PATCHLIST。
"isoline"の場合は2、TopologyはD3D_PRIMITIVE_TOPOLOGY_2_CONTROL_POINT_PATCHLIST。

この法則を崩して別な呼び方もあるかもしれないけど、今のところこの呼び方だけ確認した。コントロールポイントの定義は32まである。

partitioning

partitioningに設定できる値、integer、fractional_even、fractional_odd、pow2。
分割時の係数を整数(integer、pow2)で見るのか、小数(fractional_even、fractional_odd)で見るのかの違いと、係数の最小値の違いがあったけど、integerとpow2の違いはよくわからなかった。

outputtopology

outputtopologyに設定できる値はpoint(点)、line(線)、triangle_cw(面:時計回り)、triangle_ccw(面:反時計回り)
点と線を作り出すパターンはよくわからず。
domainを"tri"にした場合は"triangle_cw"で、"quad"にした場合は"triangle_ccw"にしないと、元々の面と向きが反対になった。

outputcontrolpoints

出力するコントロールポイントの数。入力のコントロールポイントの数と異なっても良いらしいが、そのパターンは未確認。

patchconstantfunc

パッチ定数関数名を指定する。

引数

InputPatch<VSの出力構造体, 入力コントロールポイント数>
この引数が必須で後はSV_系の引数がオプションで使える。
使えるセマンティックはSV_PrimitiveIDとSV_OutputControlPointID。
コントロールポイントが3の場合、SV_PrimitiveIDとSV_OutputControlPointIDは、(0,0),(0,1)(0,2),(1,0),(1,1)(1,2)...という値を受け取る。

パッチ定数関数定義

	struct CHSOutput
	{
		float Edge[4]	: SV_TessFactor ;
		float Inside[2]	: SV_InsideTessFactor ;
	} ;
	CHSOutput CHSMain( InputPatch<VSOutput, 4> IP, uint PatchID : SV_PrimitiveID )
	{
		CHSOutput Out ;
		Out.Edge[0] = Out.Edge[1] = Out.Edge[2] = Out.Edge[3] = gHSParam.EdgeFactor ;
		Out.Inside[0] = Out.Inside[1] = gHSParam.InsideFactor ;
		return Out ;
	}

引数

InputPatch<VSの出力構造体, 入力コントロールポイント数>
メイン関数と同様の引数。それ以外のSV_系の引数はSV_PrimitiveIDが使える。
呼ばれる単位はパッチID単位なので、SV_OutputControlPointIDを指定するとコンパイルエラーになる。

戻り値

これは、メイン関数のdomainにより決まる。
"quad"にした場合
	float Edge[4]	: SV_TessFactor ;
	float Inside[2]	: SV_InsideTessFactor ;
分割係数を設定する際のヘルパ関数

"tri"にした場合
	float Edge[3]	: SV_TessFactor ;
	float Inside	: SV_InsideTessFactor ;
分割係数を設定する際のヘルパ関数

"isoline"にした場合
	float Edge[2]	: SV_TessFactor ;
分割係数を設定する際のヘルパ関数

試しにProcessQuadTessFactorsAvgを使ってみたけど、RawEdgeFactorsに各辺の分割数、InsideScaleに1を指定すると、RoundedInsideTessFactorsには各辺の分割数がそのまま返ってくる感じだった。各辺の分割数がバラバラの値を指定した時、平均に分割されたように見える値に調整してくれるのかな?

Domain Shader


[domain("quad")]
	DSOutput DSMain( CHSOutput In, const OutputPatch<HSOutput, 4> OP, float2 DL : SV_DomainLocation )
	{
		DSOutput Out ;
		float4 p1 = lerp( OP[1].Pos, OP[0].Pos, DL.x ) ;
		float4 p2 = lerp( OP[3].Pos, OP[2].Pos, DL.x ) ;
		Out.Pos = lerp( p1, p2, DL.y ) ;

		float2 t1 = lerp( OP[1].UV, OP[0].UV, DL.x ) ;
		float2 t2 = lerp( OP[3].UV, OP[2].UV, DL.x ) ;
		Out.UV = lerp( t1, t2, DL.y ) ;
		return Out ;
	}

引数

CHSOutput In
ハルシェーダのパッチ定数関数の出力型。

const OutputPatch<HSの出力構造体, 入力コントロールポイント数>
ハルシェーダの出力をOutputPatchで囲った型。

float2 DL : SV_DomainLocation
テッセレータの出力結果?
この引数もdomain設定により決まる。
"quad"にした場合float2、"tri"にした場合float3、"isoline"にした場合float2。
この値を元に、出力の頂点を補間する。


quadの動作結果


domainを"quad"、partitioningを"integer"で動作させた結果。

EdgeFactor、InsideFactor共に1を指定した場合、分割は行われず元のまま。

EdgeFactorを1ずつ上げていくとこの様に辺の部分が指定した分割数になる。
最大64分割まで可能で、それ以上の値を指定しても64分割となる。
整数なので、1.1と2も同じ扱いで、小数点以下は切り上げられるようだ。

InsideFactorを1ずつ上げていくとこの様に内側に四角が出来てくる。辺の場合は数えられたが、Insideの方はどこをどう数えればいいのか分からない。
最大64分割まで可能で、それ以上の値を指定しても64分割となる。

EdgeFactorとInsideFactor両方とも同じ様に上げていくと、全体がきれいな分割になる。

fractional_even

fractional_odd
integerをそれぞれ、fractional_even、fractional_oddに変えるとこの様になる。
fractional_evenの方は、EdgeFactorの最小値が2となり、1を指定しても2を指定してもこの形になる。2を超えたところから変化が始まる。

fractional_even

EdgeFactorのみ、InsideFactorのみ、EdgeFactorとInsideFactorの値を動かしたときの分割結果。

fractional_odd

EdgeFactorのみ、InsideFactorのみ、EdgeFactorとInsideFactorの値を動かしたときの分割結果。InsideFactorが1のままEdgeFactorを上昇させても変化しない為、EdgeFactorのみ上昇時はInsideFactorは2に設定。

triの動作結果


fractional_even

EdgeFactorのみ、InsideFactorのみ、EdgeFactorとInsideFactorの値を動かしたときの分割結果。

fractional_odd


EdgeFactorのみ、InsideFactorのみ、EdgeFactorとInsideFactorの値を動かしたときの分割結果。InsideFactorが1のままEdgeFactorを上昇させても変化しない為、EdgeFactorのみ上昇時はInsideFactorは2に設定。

今回の嵌まりポイント


ハルシェーダのパッチ定数関数を定義して、patchconstantfuncでも指定しているにも関わらず、コンパイルエラーで定義されていないと言われ続けた。
関数の引数は構造体で定義して1つだけにしてスッキリさせていたんだけど、ハルシェーダとドメインシェーダの場合どうやら対応してないみたいで、引数にInputPatch、OutputPatchの引数があることが条件になっているのかもしれない。
サンプルの通り引数を並べてみたらコンパイルが通った。

0 件のコメント:

コメントを投稿