Direct3D11/DirectX11 (9) テセレータによるアウトラインフォントの描画など

●Tessellator のタイプ

Tessellator の出力には 3種類のタイプがあります。

[domain(type)]:
 isoline  TessFactor[2]                           出力 point line
 tri      TessFactor[3]   InsideTessFactor[1]     出力 point line triangle
 quad     TessFactor[4]   InsideTessFactor[2]     出力 point line triangle

2008-11-17追記:間違いを修正しました。上記赤字と、tri/quad では line を出力できませんでした。

quad, tri はそれぞれ四角パッチ、三角パッチを意味しています。
isoline は面を均等に割った 2次元頂点を出力するようです。
TessFactor[1] が 1.0 の場合、線分の分割に相当します。

例えば isoline で、TessFactor=8,4 を与えると下記の点を出力します。

isoline point

Tessellator が出力するプリミティブの形状は次の4種類です。

[outputtopology(type)]:
 point
 line
 triangle_cw
 triangle_ccw

これらは Hull Shader のアトリビュートで指定します。
出力トポロジの影響を直接受けるのは Geometry Shader 以降ですが、その設定も
Domain Shader ではなく HullShader が行うので注意。

ちなみに DomainShader は、プリミティブの形状 (outputtopology) を判別する
手段がありません。
DomainShader は TessFactor を参照できるため、上の domain のみ指定する必要
があります。

isoline では triangle を出力できません。
line を指定すると、単なる横線が複数出力されました。

●TessFactor の種類

partitioning は TessFactor による分割方法を指定します。

[partitioning(type)]:
 integer
 fractional_even
 fractional_odd
 pow2

fractional だと、TessFactor が実数値の場合間を補間します。
LOD のつながりがよりスムーズになると考えられます。

outputtopology も、partitioning もマニュアルに書いてあることと違います。
まだまだ間違いが多いようです。

●アウトラインフォントの描画

テセレータパイプラインは、HullShader と DomainShader のおかげでかなり自由度が
高くなっています。固定機能の制限は小さく、さまざまな応用が考えられます。

以前「3D だけでなく、ベクタフォントを GPU で直接描画するなど 2D 面でも応用できる
かもしれません。」
などと書いてしまったので実際にやってみました。

tessellator font step 1
tessellator font step 8

拡大とワイヤー表示は次の通り

tessellator font step 8
tessellator font wire

実際の補間は DomainShader が行っているため、分割したポリゴンのエッジだけ
細分化することは思ったより簡単にできそうです。
quiad や triangle で特定のエッジだけ割っても良いのですが、内部に出来た頂点が
無駄になるし、割ったエッジに影響が出ないよう一点に集めないといけないので
少々複雑です。

isoline があるので、テセレータの段階では輪郭をそのまま割るだけにします。
ポリゴン化は GeometryShader で行います。

簡単にするため、データは前処理で専用のものを用意します。
3次ベジェ のアウトラインを使っています。

tess_font point

各区間、A-B や B-C はそれぞれ 2点の頂点座標(アンカー)+ハンドル2点 で表現
できます。各区間を 4点の制御座標でデータ化します。
このとき隣接するアンカーポイント(頂点)は共有可能です。

A-B :  頂点A, 制御点A0, 制御点A1, 頂点B
B-C :  頂点B, 制御点B0, 制御点B1, 頂点C

これをそのまま D3D11_PRIMITIVE_TOPOLOGY_4_CONTROL_POINT_PATCHLIST
で渡し、domain(“isoline”)、outputtopology(“line”) で描画すれば容易にアウトラインの
線描画ができます。DomainShader でベジェカーブを求めるだけです。
フォントのアウトラインそのままで追加情報は不要です。

塗りつぶしは少々手間がかかります。上図の X, Y のように前処理で描画用の
追加点が必要です。

A-B は A-X-B の 3角形、B-C は B-X-Y-C の 4角形を描画します。
処理を簡単にするため、A-X-B は X が2頂点重なってると見なしてすべて四角形で行います。

これらの追加頂点を分割面 (A-X-B や B-X-Y-C) 毎に保持し、GeometryShader まで
情報が届くようにします。

必要な座標は 2次元 x,y のみです。
float4 にすると z,w にもう 1つ座標を入れられます。
アンカーポイントは共有される可能性があるため、面の情報はハンドル(制御点)側の
頂点 zw に入れます。

B-C の例、4頂点

 pointB.xy = 頂点B
 pointB.zw = 未使用

 ctrlB0.xy = 制御点B0
 ctrlB0.zw = 追加の頂点、追加点X

 ctrlB1.xy = 制御点B1
 ctrlB1.zw = 追加の頂点、追加点Y

 pointC.xy = 頂点C
 pointC.zw = 未使用

頂点フォーマットは単純な float4 のみ

D3D11_INPUT_ELEMENT_DESC	ldesc[]= {
{ "VPOS", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 0,
	D3D11_INPUT_PER_VERTEX_DATA, 0	},
};
iDevice->CreateInputLayout( ldesc, 1, codeblob->GetBufferPointer(),
				codeblob->GetBufferSize(), &iLayout );

VertexShader はデータにスケーリング補正してるだけでそのまま流します。

// vs.hlsl
struct VS_INPUT {
	float4	vPos	: VPOS;
};

float4 main( VS_INPUT idata ) : VSPOS
{
	float4	pos= idata.vPos;

	const float	SCALE= 0.08;
	const float2	CENTER= float2(137,212);
	pos-= CENTER.xyxy;
	pos*= SCALE.xxxx;
	return	pos;
}

HullShader もテセレータの設定のみ。
分割は線分で行うので domain(“isoline”) を使い、GeometryShader で 2点のつながり
が必要なので出力は outputtopology(“line”)。
コントロールポイントはそのままスルーで DomainShader に任せます。

// hs.hlsl
struct VS_OUTPUT {
	float4	vPos	: VSPOS;
};

struct HS_DATA {
	float	Edge[2]	: SV_TessFactor;
};

HS_DATA func()
{
	HS_DATA	pdata;
	pdata.Edge[0]= 8.0;	// ここで分割数の決定、値が大きいとなめらか
	pdata.Edge[1]= 1.0;
	return	pdata;
}

[domain("isoline")]
[partitioning("integer")]
[outputtopology("line")]
[outputcontrolpoints(4)]
[patchconstantfunc("func")]
float4 main( InputPatch ip,
	uint id : SV_OutputControlPointID ) : TSPOS
{
	return	ip[id].vPos;
}

DomainShader で分割点のベジェ補間を行い、GeometryShader に渡すための頂点を
構築します。
追加点は 2番目と 3番目の zw に入っているので、それらも取り出して頂点に入れます。
uv は Geometry Shader で線分の最後かどうか判断するために使います。

// ds.hlsl
struct HS_DATA {
	float	Edge[2]		: SV_TessFactor;
};

struct HS_OUTPUT {
	float4	vPos	: TSPOS;
};

cbuffer perFrame {
	float4x4	WVP;
};

float4 Bezier0( float t )
{
	float	t2= 1.0-t;
	return	float4( t2*t2*t2, t2*t2* t*3, t2* t* t*3, t* t* t );
}

float2 UVtoPosition( const OutputPatch p, float2 uv )
{
	float4	u= Bezier0( uv.x );
	return	 u.x*p[0].vPos.xy
		+u.y*p[1].vPos.xy
		+u.z*p[2].vPos.xy
		+u.w*p[3].vPos.xy;
}

struct DS_OUTPUT {
	float4	nPos[2]	: NPOS;
	float	uv	: UV;
	float4	vPos	: SV_Position;
};

[domain("isoline")]
DS_OUTPUT main(
	HS_DATA ip,
	float2 uv : SV_DomainLocation,
	const OutputPatch bpatch )
{
	DS_OUTPUT	dsout;

	float2	pos= UVtoPosition( bpatch, uv );
	dsout.vPos= mul( float4( pos.xy, 0, 1 ), WVP );

	// 追加点 2つを各頂点に埋め込む
	dsout.nPos[0]= mul( float4( bpatch[1].vPos.zw, 0, 1 ), WVP );
	dsout.nPos[1]= mul( float4( bpatch[2].vPos.zw, 0, 1 ), WVP );

	dsout.uv= uv.x; // Geometry Shader で使う
	return	dsout;
}

GeometryShader は頂点情報から Triangle を作ります。

// gs.hlsl
struct DS_OUTPUT {
	float4	nPos[2]	: NPOS;
	float	uv	: UV;
	float4	vPos	: SV_Position;
};

[maxvertexcount(6)]
void main(
	line DS_OUTPUT input[2],
	inout TriangleStream stream )
{
	stream.Append( input[0] );
	stream.Append( input[1] );

	DS_OUTPUT	dout2= input[0];
	dout2.vPos= input[0].nPos[0];
	stream.Append( dout2 );

	stream.RestartStrip();

	if( input[1].uv >= 0.9998f ){
		DS_OUTPUT	dout3= input[1];
		dout3.vPos= input[0].nPos[1];

		stream.Append( input[1] );
		stream.Append( dout2 );
		stream.Append( dout3 );
		stream.RestartStrip();
	}
}

uv で判断しているのは最後だけ 1ポリゴン追加する必要があるためです。
下図でいう 4 番。

tess font

描画自体は簡単にできました。
元となるデータを準備する方が大変です。
中心に位置する追加点を求める何らかの手段が必要となりそうです。

関連エントリ
Direct3D11/DirectX11 (8) テセレータの動作
Direct3D11/DirectX11 (7) テセレータの流れの基本部分
Direct3D11/DirectX11 (6) D3D11 の ComputeShader を使ってみる
Direct3D11/DirectX11 (5) WARP の試し方、Dynamic Shader Linkage
Direct3D11/DirectX11 (4) FeatureLevel と旧 GPU の互換性、テクスチャ形式など
Direct3D11 (DirectX11) シェーダーの書き込み RWBuffer 他
Direct3D11 の遅延描画、スレッド対応機能、シェーダー命令
Direct3D11 Technical Preview D3D11の互換性、WARP Driver