Direct3D11/DirectX11 (15) GPU を使ったアウトラインフォントの描画の(3)

↓手書きのフォントもベクタ表示
tess font 40

続きです。
前回表示したアウトラインフォント のシェーダー側の動作となります。

入力データは quad のエッジ情報です。通常は VertexShader に頂点情報を渡しますが、
接続する 2頂点+2コントロールポイント座標を一組としてシェーダーに渡します。

1つの quad を描画するためには 4辺分+中心座標が必要なので、これが 5組あります。
よって描画データは下記の通り。

// 1 quad 分
v0 c0 c1 v1
v0 c0 c1 v1
v0 c0 c1 v1
v0 c0 c1 v1
g0 0  0  0

 v0= x,y 頂点
 c0= x,y コントロールポイント
 g0= x,y 中心座標
 0= 0,0 未使用

ラインの場合コントロールポイントが不要なので c0, c1 は使いません。
中心座標は x,y のみなので、残りは全部 0 です。

カーブの場合: v0 c0 c1 v1
ラインの場合: v0 -1e5 0 v1

例
249.593597f,395.421600f,  302.652008f,346.582397f,  341.562408f,290.375214f,  366.312805f,226.812805f,
366.312805f,226.812805f,  -1e5f,0,  0,0,  324.124786f,219.500000f,
324.124786f,219.500000f,  298.250397f,282.316803f,  263.750397f,333.031189f,  220.624802f,371.656006f,
220.624802f,371.656006f,  -1e5f,0,  0,0,  249.593597f,395.421600f,
293.008575f,305.777374f,  0,0, 0,0, 0,0,

IA に渡す頂点の定義は次の通り。float2 × 4

D3D11_INPUT_ELEMENT_DESC	ldesc[]= {
{  "VPOS", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 0,
				D3D11_INPUT_PER_VERTEX_DATA, 0	},
{  "VPOS", 1, DXGI_FORMAT_R32G32_FLOAT, 0, sizeof(float)*2,
				D3D11_INPUT_PER_VERTEX_DATA, 0	},
{  "VPOS", 2, DXGI_FORMAT_R32G32_FLOAT, 0, sizeof(float)*4,
				D3D11_INPUT_PER_VERTEX_DATA, 0	},
{  "VPOS", 3, DXGI_FORMAT_R32G32_FLOAT, 0, sizeof(float)*6,
				D3D11_INPUT_PER_VERTEX_DATA, 0	},
};

これが 5 個なので D3D11_PRIMITIVE_TOPOLOGY_5_CONTROL_POINT_PATCHLIST
で描画します。

Vertex Shader は素通り

struct VS_INPUT {
	float2	vPos0	: VPOS0;
	float2	vC0	: VPOS1;
	float2	vC1	: VPOS2;
	float2	vPos1	: VPOS3;
};
struct VS_OUTPUT {
	float2	vPos0	: VSPOS0;
	float2	vC0	: VSPOS1;
	float2	vC1	: VSPOS2;
	float2	vPos1	: VSPOS3;
};

VS_OUTPUT main( VS_INPUT idata )
{
	VS_OUTPUT	odata;
	odata.vPos0= idata.vPos0;
	odata.vPos1= idata.vPos1;
	odata.vC0= idata.vC0;
	odata.vC1= idata.vC1;
	return	odata;
}

Hull Shader はカーブかラインか区別して異なる TessFactor を設定します。

#define	MAX_POINTS	5

struct VS_OUTPUT {
	float2	vPos0	: VSPOS0;
	float2	vC0	: VSPOS1;
	float2	vC1	: VSPOS2;
	float2	vPos1	: VSPOS3;
};

struct HS_OUTPUT {
	float2	vPos0	: HSPOS0;
	float2	vC0	: HSPOS1;
	float2	vC1	: HSPOS2;
	float2	vPos1	: HSPOS3;
};


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

HS_DATA func( InputPatch ip )
{
	HS_DATA	pdata;
	const float	tf= 8.0;
	const float	lineFlag= -1e4;
	// ラインを区別する
	pdata.Edge[3]= ip[0].vC0.x > lineFlag ? tf : 1.0f;
	pdata.Edge[2]= ip[1].vC0.x > lineFlag ? tf : 1.0f;
	pdata.Edge[1]= ip[2].vC0.x > lineFlag ? tf : 1.0f;
	pdata.Edge[0]= ip[3].vC0.x > lineFlag ? tf : 1.0f;
	// 内部は 2固定
	pdata.Inside[0]= 2.0;
	pdata.Inside[1]= 2.0;
	return	pdata;
}

[domain("quad")]
[partitioning("integer")]
[outputtopology("triangle_cw")]
[outputcontrolpoints(5)]
[patchconstantfunc("func")]
HS_OUTPUT main(
	InputPatch ip,
	uint id : SV_OutputControlPointID
	)
{
	HS_OUTPUT	odata;
	odata.vPos0= ip[id].vPos0;
	odata.vPos1= ip[id].vPos1;
	odata.vC0= ip[id].vC0;
	odata.vC1= ip[id].vC1;
	return	odata;
}

Doman Shader は生成された頂点がどのエッジか区別して、それぞれ異なるパラメータ
補間を適用します。
InsideTessFactor が 2.0 なので、Tessellator で分割されるのはエッジだけです。
上下左右のエッジは

・u が 0.0
・u が 1.0
・v が 0.0
・v が 1.0

で区別することができます。上のどれでもない場合は中心座標です。

例として 1辺だけ TessFactor[0] = 4 の場合を考えます。
このとき生成される頂点の uv は下記の 8点となります。

四隅
(0.0, 0.0)
(1.0, 0.0)
(0.0, 1.0)
(1.0, 1.0)

中心
(0.5, 0.5)

分割で挿入された点
(0.0, 0.25)
(0.0, 0.50)
(0.0, 0.75)

よって u が 0.0 の場合を区別すれば必要なエッジだけ取り出せることがわかります。

#define	CONTROL_POINT	5

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

struct HS_OUTPUT {
	float2	vPos0	: HSPOS0;
	float2	vC0	: HSPOS1;
	float2	vC1	: HSPOS2;
	float2	vPos1	: HSPOS3;
};

struct DS_OUTPUT {
	float4	vPos	: SV_Position;
};

cbuffer pf {
	float4x4	WVP;
};

// カーブの場合
float2 UVtoPositionB( const HS_OUTPUT p, float t )
{
	float	t2= 1.0-t;
	float4	u= float4( t2*t2*t2, t2*t2* t*3, t2* t* t*3, t* t* t );
	return	 u.x*p.vPos0.xy +u.y*p.vC0.xy +u.z*p.vC1.xy +u.w*p.vPos1.xy;
}

// 直線の場合
float2 UVtoPositionL( const HS_OUTPUT p, float ux )
{
	return	p.vPos0 * (1-ux) + p.vPos1 * ux; // 実際は不要
}

float2 UVtoPosition( const HS_OUTPUT p, float ux )
{
	if( p.vC0.x < -1e4 ){
		return	UVtoPositionL( p, ux );
	}
	return	UVtoPositionB( p, ux );
}

[domain("quad")]
DS_OUTPUT main(
		HS_DATA ip,
		float2 uv : SV_DomainLocation,
		const OutputPatch bpatch
	)
{
	DS_OUTPUT	dout;
	const float	EPC= 0.0f; // 誤差が問題になるなら増やす
	float2	pos= float2( 0, 0 );
	// 各エッジを切り分ける
	if( uv.y >= 1.0-EPC ){
		pos= UVtoPosition( bpatch[0], uv.x );
	}else if( uv.y <= 0.0+EPC ){
		pos= UVtoPosition( bpatch[2], 1-uv.x );
	}else if( uv.x >= 1.0-EPC ){
		pos= UVtoPosition( bpatch[1], 1-uv.y );
	}else if( uv.x <= 0.0+EPC ){
		pos= UVtoPosition( bpatch[3],   uv.y );
	}else{
		pos= bpatch[4].vPos0.xy; // 中心
	}
	dout.vPos= mul( float4( pos.xy, 0, 1 ), WVP );
	return	dout;
}

エッジの切り分けは、四隅の頂点が共有されている点に注意です。
どの uv 値をどのエッジに割り当てるのかは任意で構いませんが、四隅が共有されている
関係上、エッジの並びが連続していなければカーブが不連続になります。

またエッジ単位に Tess Factor のスケールを入れれば、長い辺など必要な場所のみ
より細かく分割するなどの応用ができるでしょう。

前回のアルゴリズムはまだ完全に形状を分割できるとは限らず、いくつか問題が生じる
ケースがあります。

(1) 分割時に対応する頂点が見つけられない場合がある
(2) アウトラインからはみ出さない位置に中心頂点を配置できない場合がある

このような状態に陥った場合、対象のプリミティブのエッジを分割して
頂点(アンカー)を追加することで対処可能です。

シェーダーを書き出していて気がつきましたが、これ GeometryShader だけでも実現
できそうですね。GeometryShader には 5 頂点入力がないので、頂点数さえ合わせれば
ハードウエアで実行できそうです。

関連エントリ
Direct3D11/DirectX11 (14) GPU を使ったアウトラインフォントの描画の(2)
Direct3D11/DirectX11 (13) TessFactor とシェーダーリンクの補足など
Direct3D11/DirectX11 (12) テセレータのレンダーステート他
Direct3D11/DirectX11 (11) 互換性とシェーダーの対応表など
Direct3D11/DirectX11 (10) テセレータの補間
Direct3D11/DirectX11 (9) テセレータによるアウトラインフォントの描画など
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