↓手書きのフォントもベクタ表示
続きです。
前回表示したアウトラインフォント のシェーダー側の動作となります。
入力データは 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( InputPatchip ) { 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 OutputPatchbpatch ) { 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