シェーダ(HLSL)でリアルタイムにベクターイメージの描画を行う


開発の環境は次の通りです。
DirectX 9、GeForce 8600 GT、Visual C++ 2010

結論から言うと実用に耐えうるものは作成できませんでした。
これ以上作成を続けても労力に見合ったものが作れる気がしないので中止します。

実用にはなりませんが、なんとなく公開しておきます。


ちなみに、これの作成を思い立ったのは去年の暮れで、
途中まったく作業していませんが5ヶ月ほどかけています。
途中Twitterにはレンダリングしたスクリーンショットをアップしていました。


このベクターイメージの描画で目標としていたのはSVGをシェーダでリアルタイムに描画することでした。
達成できたのは、パスの描画、円(楕円)、長方形を個別に描画することのみで、FPSは4.5程度しか出ておりません。
動作環境やシェーダモデルが3.0より新しいものであれば事情は変わるかもしれません。


では、これらの描画の方法について説明します。
まず、シェーダでのリアルタイムなベクター(ベジェ)の描画というのはすでに存在しています。

http://hosok2.com/project/st/st_coloration.html

この実装と特に異なるのは頂点の座標がシェーダに直接記述されているか、外から与えられるかという点です。


私の考えた方法では、頂点座標は浮動小数点数テクスチャに格納し、シェーダから参照するようにしています。
ベクターの形状を識別するためのパラメータは別の整数のテクスチャに格納し参照しています。
それぞれのテクスチャのフォーマットは、
浮動小数点数テクスチャ:D3DFMT_G32R32F
整数テクスチャ:D3DFMT_A8R8G8B8
です。

テクスチャへのデータの格納を簡単に行うために、IDirect3DTexture9を以下のクラスでラップしています。

template<class T>
class TextureData{
private:
	IDirect3DTexture9* tex;
	D3DLOCKED_RECT lockRect;
public:
	TextureData(IDirect3DTexture9* tex)
		:tex(tex)
	{
		// サーフェイスロック
		tex->LockRect(0, &lockRect, NULL, D3DLOCK_DISCARD);

		D3DSURFACE_DESC d;
		tex->GetLevelDesc(0, &d);
		// テクスチャサーフェイスの初期化
		FillMemory(lockRect.pBits, lockRect.Pitch * d.Height, 0xff);
	}
	~TextureData(){
		// サーフェイスアンロック
		tex->UnlockRect(0);
	}

	inline T& operator[](int index){
		return static_cast<T*>(lockRect.pBits)[index];
	}
	
	void set2(int index, T arg0, T arg1){
		(*this)[ index*2 ] = arg0;
		(*this)[ index*2 + 1] = arg1;
	}
};

パスの描画

パスはベジェ、直線を連結したものです。
ベジェの座標の指定についてはSVGのフォーマットを参考にしています。
http://www.hcn.zaq.ne.jp/___/REC-SVG11-20030114/paths.html#PathDataCubicBezierCommands

ベジェは始点、終点と2つの制御点から成ります。
浮動小数点数テクスチャには始点、制御点1、制御点2、終点の順に頂点座標を格納します。
直線は始点と終点のみなので、始点、終点の順に格納します。

パスの場合、ベジェに続いてベジェや直線が続くため、重複する頂点が現れます。
続くベジェの始点は、直前のベジェ(もしくは直線)の終点なので、それをそのまま利用します。
続くベジェの制御点1は、直前のベジェの制御点2を直前のベジェの終点に対して反対側にとったものになります。
浮動小数点数テクスチャには制御点2、終点を格納します。

直前が直線だった場合、制御点はその直線の終点とします。


パスの始めがベジェの場合、パスの始めが直線の場合、パスの始めでない部分に現れるベジェ(もしくは直線)
この4つのタイプの識別は整数テクスチャに格納します。

また、整数テクスチャには、それぞれの形状で使用する浮動小数点テクスチャのバッファの位置を格納し、
それを用いて座標を取得します。


パスの描画を行う場合、それぞれのテクスチャに以下の様にデータを格納します。

data[0] = 0x00010000; 	//(1)続くデータがパスであることを示す
data[1] = 0xffffffff; 	//(2)線色
data[2] = 0xffff7f00; 	//(3)塗り色
data[3] = 0x1; 			//(4)閉じる

data[4] = 0;	//(5)パスの初めがベジェ

data[5] = 0;	//(6)浮動小数点数テクスチャの位置
data2.set2(0, 0.1f, 0.5f); //(7)浮動小数点数テクスチャの0番目のテクセル
data2.set2(1, 0.1f, 0.9f);
data2.set2(2, 0.4f, 0.9f);
data2.set2(3, 0.5f, 0.5f);

data[6] = 1;	//(8)次はベジェ

data[7] = 4;
data2.set2(4, 0.9f, 0.1f);
data2.set2(5, 0.9f, 0.5f);

data[8] = 3;

data[9] = 6;
data2.set2(6, 0.9f, 0.9f);

data[10] = 0x02000000; //(9)パスの終了

(1)まず、形状を識別するための値を頭に入れます。0x10000はパスのデータが格納されていることを示します。
線の色(2)、塗りの色(3)を指定します。
(4)これはパス始点と終点が結合していない場合の処理で、1であれば始点と終点に直線を引きます。
(5)続く頂点データの意味を指定します。
パスの始めのベジェであれば0、途中に現れるベジェであれば1、パスの始めの直線であれば2、途中に現れる直線であれば3
(6)頂点データの浮動小数点数テクスチャでの位置を指定します。
(7)この場合浮動小数点数テクスチャの0番目のテクセルのRに0.1f、Gに0.5fを格納します。
それぞれシェーダではx,yとして取得することができます。
(8)同様に途中に現れるベジェの指定です。
(9)パスの終了です。


シェーダプログラムでは、まず最初の4つの整数を読み取った後、それぞれの形状によって処理を分けながら
整数を読み進めていきます。パスの場合は0x02000000が現れるまでこの処理を反復しますが、
シェーダプログラムでは無限ループを指定できないため6回のループに制約しています。
0x02000000を読み取る前でも、たくさん接続されたパスは途中で切れてしまうことを意味します。

>パスの描画、円(楕円)、長方形を個別に描画することのみ
冒頭でこのように記述したのも無限ループを作れないため、単独で描画することしかできないためです。
無限ループが作れないのは、おそらく最適化でループを展開しているためだと思うのですが、
6回に制約したループであっても(最適化の所為かわかりませんが)、コンパイルに6分程度の時間がかかってしまいます。

矩形の描画

data[0] = 0x00010001; //矩形
data[1] = 0xffffffff; //線色
data[2] = 0x7f7fff00; //塗り色

data[3] = 0;		//浮動小数点数テクスチャの位置
data2.set2(0, 0.5f, 0.5f); //位置
data2.set2(1, 0.1f, 0.3f); //幅、高さ
data2.set2(2, 3.14f/4.f, 0); //回転(第3引数は未使用)

パスと違いシンプルです。
楕円の描画はdata[0]の0x00010001を0x00010002に置き換えるだけです。
(0x00010000でも0x00010001でもなければ0x00010002でなくてもいいです)



最後にこれらのテクスチャをシェーダに設定します。ほかにもいくつかのパラメータを設定します。

eff->SetTexture( "tex0", texture ); //整数テクスチャ
eff->SetTexture( "ftex", texture2 ); //浮動小数点数テクスチャ
eff->SetInt( "TextureResolution", 16 ); //テクスチャ解像度
eff->SetInt( "g_devide", 18 ); //ベジェの分解能

effはID3DXEffectです。
ベジェは直線に分割して描画することになるので、その分割の数がベジェの分解能です。
ベジェの分解能は18以上の値を入れても18に制限されます。



今回作成したシェーダプログラムです。

float4x4 World;
float4x4 View;
float4x4 Projection;
int TextureResolution;
int g_devide;

texture tex0;
sampler tex_sampler = sampler_state
{
    Texture = <tex0>;
    MinFilter = POINT;
    MagFilter = POINT;
    MipFilter = POINT;
    AddressU = Clamp;
    AddressV = Clamp;
};

texture ftex;
sampler ftex_sampler = sampler_state
{
    Texture = <ftex>;
    MinFilter = POINT;
    MagFilter = POINT;
    MipFilter = POINT;
    AddressU = Clamp;
    AddressV = Clamp;
};
float3x3 transform : TRANSFORM;
float2 screen : SCREEN;

struct VSO
{
	float4 Position : POSITION;
	float2 UV : TEXCOORD0;
	float3 Normal : TEXCOORD1;
	float4 Color : COLOR0;
};
 
VSO mainVS(float4 position : POSITION,
                    float2 tex : TEXCOORD0,
					float4 color : COLOR0,
					float3 normal : TEXCOORD1)
{
    VSO output;
    float4 worldPosition = mul(position, World);
    float4 viewPosition = mul(worldPosition, View);
    output.Position = mul(viewPosition, Projection);
	output.UV = tex;
	output.Color = color;
	output.Normal = normal;
    return output;
}

int toint(float4 f4){
	return (int)(f4.a * 256 * 256 * 256 * 255 +
	f4.r * 256 * 256 * 255 +
	f4.g * 256 * 255 +
	f4.b * 255);
}

float4 bezierColor(float2 UV, float4 lc, float4 fc,
				float2 p0, float2 p1, float2 p2, float2 p3) : COLOR0;
int bezierColorFill(float2 UV, float4 lc, float4 fc,
				float2 p0, float2 p1, float2 p2, float2 p3);
float2 getBezier(float2 p0, float2 p1, float2 p2, float2 p3, float t);
bool isinline(float2 v0, float2 v1, float2 p);
float linepoint_closest(float2 v0, float2 v1, float2 p);
bool islinecross_scan(float2 v0, float2 v1, float y);
float linecross_left(float2 v0, float2 v1, float y);

float4 rectangleColor(float2 UV, float4 lc, float4 fc,
				float2 p0, float2 p1, float2 p2) : COLOR0;
float4 circleColor(float2 UV, float4 lc, float4 fc,
				float2 p0, float2 p1, float2 p2) : COLOR0;

int fetchint(int index){
	return toint(tex2D(tex_sampler, float2((float)(index % TextureResolution)/(float)TextureResolution,
								(float)(index / TextureResolution)/(float)TextureResolution)));
}
float4 fetch(int index){
	return tex2D(tex_sampler, float2((float)(index % TextureResolution)/(float)TextureResolution,
								(float)(index / TextureResolution)/(float)TextureResolution));
}
float2 ffetch(int index){
	return tex2D(ftex_sampler, float2((float)(index % TextureResolution)/(float)TextureResolution,
							(float)(index / TextureResolution)/(float)TextureResolution)).xy;
}

float4 mainPS(VSO input) : COLOR0
{
	int maxloop = 6;
	int tag = fetchint(0);
	// path
	if( tag == 0x00010000L ){
		float4 lc = fetch(1);
		float4 fc = fetch(2);
		int close = fetchint(3);
		int index = 4L;
		float2 startpos = float2(0, 0);
		float2 lastp = float2(0, 0);
		float2 controlp = float2(0, 0);
		int crosscnt = 0;
		while(maxloop-- > 0)
		{
			int s = fetchint(index++);
			int startindex = fetchint(index++);
			float2 p0, p1;
			if(s == 0L || s == 1L){
				if(s == 0L){
					p0 = startpos = ffetch(startindex); startindex++;
					p1 = ffetch(startindex); startindex++;
				}else{
					p0 = lastp;
					p1 = controlp;
				}
				float2 p2 = ffetch(startindex); startindex++;
				float2 p3 = lastp = ffetch(startindex);
				controlp = lastp + -(p2 - p3);
				float4 c = bezierColor(input.UV, lc, fc, p0, p1, p2, p3);
				if( c.a != 0 ) return c;
				crosscnt += bezierColorFill(input.UV, lc, fc, p0, p1, p2, p3);
			}else if(s == 2L || s == 3L){
				if( s == 2L ){
					p0 = startpos = ffetch(startindex); startindex++;
					p1 = lastp = controlp = ffetch(startindex);
				}else{
					p0 = lastp;
					p1 = lastp = controlp = ffetch(startindex);
				}
				
				float len = length( input.UV - p0 );
				if( len <= 0.01 )
					return lc;
				
				len = length( input.UV - p1 );
				if( len <= 0.01 )
					return lc;
					
				if( isinline( p0, p1, input.UV ) ){
					if( linepoint_closest( p0, p1, input.UV ) < 0.01 ){
						return lc;
					}
				}
				
				if( islinecross_scan( p0, p1, input.UV.y ) ){
					if( linecross_left( p0, p1, input.UV.y ) < input.UV.x ){
						crosscnt++;
					}
				}
			}else if(s == 0x02000000L){
				break;
			}
		}
		
		// パスが閉じる指定の場合ここでライン描画されるかチェックする
		if( close != 0 && isinline( startpos, lastp, input.UV ) ){
			if( linepoint_closest( startpos, lastp, input.UV ) < 0.01 ){
				return lc;
			}
		}
		
		// 塗りを行う前にパスを閉じるラインを考慮してカウントする
		if( islinecross_scan( startpos, lastp, input.UV.y ) ){
			if( linecross_left( startpos, lastp, input.UV.y ) < input.UV.x ){
				crosscnt++;
			}
		}
		if( (crosscnt % 2) != 0 ) return fc;
	// rectangle & circle
	}else{
		float4 lc = fetch(1);
		float4 fc = fetch(2);
		int startindex = fetchint(3);
		float2 pos = ffetch(startindex++);
		float2 radius = ffetch(startindex++);
		float2 radian = ffetch(startindex);
		float4 c;
		if( tag == 0x00010001L ){ // rectangle
			c = rectangleColor( input.UV, lc, fc, pos, radius, radian );
		}else{ // circle
			c = circleColor( input.UV, lc, fc, pos, radius, radian );
		}
		if( c.a != 0 ) return c;
	}
	
	return float4(0, 0, 0, 0);
}

float4 bezierColor(float2 UV, float4 lc, float4 fc,
				float2 p0, float2 p1, float2 p2, float2 p3) : COLOR0
{
	int devide = min(g_devide, 18);
	int i;
	// line connect circle
	for(i=0; i<devide + 1; i++){
		float2 p = getBezier(p0, p1, p2, p3, (float)i/(float)devide);
		float len = length( UV - p );
		if( len <= 0.01 )
			return lc;
	}
	// line
	float2 p1_ = getBezier(p0, p1, p2, p3, 0);
	for(i=0; i<devide; i++){
		float2 p0_ = p1_;
		p1_ = getBezier(p0, p1, p2, p3, (float)(i+1)/(float)devide);
		if( isinline( p0_, p1_, UV ) ){
			if( linepoint_closest( p0_, p1_, UV ) < 0.01 ){
				return lc;
			}
		}
	}

	return float4(0, 0, 0, 0);
}


int bezierColorFill(float2 UV, float4 lc, float4 fc,
				float2 p0, float2 p1, float2 p2, float2 p3)
{
	int devide = min(g_devide, 18);
	// fill bezier
	int crosscnt = 0;
	float2 p1_ = getBezier(p0, p1, p2, p3, 0);
	for(int i=0; i<devide; i++){
		float2 p0_ = p1_;
		p1_ = getBezier(p0, p1, p2, p3, (float)(i+1)/(float)devide);
		if( islinecross_scan( p0_, p1_, UV.y ) ){
			if( linecross_left( p0_, p1_, UV.y ) < UV.x ){
				crosscnt++;
			}
		}
	}
	return crosscnt;
}

float2 getBezier(float2 p0, float2 p1, float2 p2, float2 p3, float t){
	float2 v0 = p1 - p0;
	float2 v1 = p2 - p1;
	float2 v2 = p3 - p2;

	float2 p4 = (p0 + v0 * t);
	float2 p5 = (p1 + v1 * t);
	float2 p6 = (p2 + v2 * t);

	float2 v3 = p5 - p4;
	float2 v4 = p6 - p5;
	
	float2 p7 = (p4 + v3 * t);
	float2 p8 = (p5 + v4 * t);

	float2 v5 = p8 - p7;
	
	return p7 + v5 * t;
}

bool isinline(float2 v0, float2 v1, float2 p){
	float2 va = v1 - v0;
	float2 va_ = normalize( va );
	float2 vb = p - v0;

	float d = dot( va_, vb );
	if( d < 0 ) return false;

	return d <= length( va );
}
float linepoint_closest(float2 v0, float2 v1, float2 p){
	float2 va = v1 - v0;
	float2 vb = p - v0;
	return abs( va.x * vb.y - va.y * vb.x ) / length( va );	
}

//スキャンラインと線分がy成分的に交差する可能性があるか
bool islinecross_scan(float2 v0, float2 v1, float y){
	return max( v0.y, v1.y ) >= y && min( v0.y, v1.y ) < y;
}
//直線とスキャンラインの最左端取得
float linecross_left(float2 v0, float2 v1, float y){
	if( v1.y == v0.y ) return max(v0.x, v1.x);
	return (-(v0.x - v1.x) * y - v1.x*v0.y + v0.x*v1.y) / (v1.y - v0.y);
}

float4 rectangleColor(float2 UV, float4 lc, float4 fc,
				float2 p0, float2 p1, float2 p2) : COLOR0
{
	float s = sin(-p2.x);
	float c = cos(-p2.x);
	float2x2 m = { c, s, -s, c };
	UV -= p0;
	UV = mul(UV, m);
	
	UV.x = abs(UV.x) - p1.x;
	UV.y = abs(UV.y) - p1.y;
	
	if( UV.x < - 0.01 && UV.y < - 0.01 ) return fc;		
	if( UV.x < 0.01 && UV.y < 0.01 ) return lc;	
	return float4(0, 0, 0, 0);
}

float4 circleColor(float2 UV, float4 lc, float4 fc,
				float2 p0, float2 p1, float2 p2) : COLOR0
{
	float s = sin(-p2.x);
	float c = cos(-p2.x);
	float2x2 m = { c, s, -s, c };
	UV -= p0;
	UV = mul(UV, m);
	p1 -= float2( 0.01, 0.01 );
	float r = (UV.x * UV.x)/(p1.x * p1.x) + (UV.y * UV.y)/(p1.y * p1.y);
	if( r <= 1 ) return fc;
	p1 += float2( 0.02, 0.02 );
	r = (UV.x * UV.x)/(p1.x * p1.x) + (UV.y * UV.y)/(p1.y * p1.y);
	if( r <= 1 ) return lc;
	return float4(0, 0, 0, 0);
}

technique tec0
{
    pass p0
    {
        VertexShader = compile vs_3_0 mainVS();
        PixelShader   = compile ps_3_0 mainPS();
    }
}