PixelShaderを使った画像補間 (2) 三次補間(Bi-Cubic)

というわけで、二回目は三次補間をやってみたいと思います。PixelShaderのプログラムもちゃんと出ますので。

一応前回の記事にも目を通しておいてほしいです。

三次補間の定義式

三次補間は重み係数w(t)を以下のように計算します。

w(t)=¥begin{cases} (a+2)|t|^3-(a+3)|t|^2+1 & (|t| ¥le 1) ¥¥ a|t|^3-5a|t|^2+8a|t|-4a & (1 < |t| ¥le 2) ¥¥ 0 & (|t| > 2) ¥end{cases}

なお、この式の中で出てくるaは重みの調整用係数で、通常は-0.5~-1.0の範囲で使用されます。-1.0に近くなるほど絵はシャープに補間されます。

三次補間で使われるのは4×4の範囲ですので、¥mathbf{P}は4次の正方行列になります。中心点は行列の要素では(1,1)ですね。

というわけで、PixelShaderの本体部

解説しながらいきますので、細切れに表示していきます。さくっとコンパイルしたい方はそれぞれを貼り付けてひとつのファイルにすればOKです。

座標情報定義

struct VS_OUTPUT{
	float4 Pos : POSITION;
	float2 Tex0 : TEXCOORD0; //(-1,-1)
	float2 Tex1 : TEXCOORD1; //(-1, 0)
	float2 Tex2 : TEXCOORD2; //(-1, 1)
	float2 Tex3 : TEXCOORD3; //(-1, 2)
	float2 Tex4 : TEXCOORD4; //( 0,-1)
	float2 Tex5 : TEXCOORD5; //( 0, 0)
	float2 Tex6 : TEXCOORD6; //( 0, 1)
	float2 Tex7 : TEXCOORD7; //( 0, 2)
};

VertexShader(というより頂点処理)が完了したときの頂点情報の定義です。この中で問題なのは「なぜテクスチャ座標が8つも存在するのか?」ということです。

理由は簡単で、「PixelShaderでの座標位置計算をできるだけ少なくするため」です。8点をあらかじめ計算してあれば残りは8点になるので簡単な計算で出せますが、

描画元位置1点だけでは残りの座標をすべて計算しなければならず、演算スロットが不足する、ということです。

もう一つ、8点が「上限」の理由は、PixelShader2.0に対応している場合、最低でも8点のテクスチャ座標を受け入れる、という仕様があるためです。

なので、8点より多く座標を書くことは互換性を失わせることになるのでこれが限界になります。

コメントで書いてある座標は、それぞれが基準座標に対してどれくらい「ずれた」座標を指しているか、という意味です。

Tex5は前回の描画元画素行列、¥mathbf{P}においてちょうどP_{k,l}を参照している場所になります。

Sampler定義

Texture Tex;
sampler Samp = sampler_state
{
	Texture = <Tex>;
	MinFilter = POINT;
	MagFilter = POINT;
	MipFilter = NONE;
	AddressU = Clamp;
	AddressV = Clamp;
};

Samplerの宣言はほとんど普通ですが、二つ制約があります。

  • MinFilterとMagFilterはPOINT(補間なし)であること
  • AddressUとAddressVはClamp(はみ出した座標を指定されたときはエッジの色を参照する)であること

補間処理をPixelShaderで行うのでもちろん補間フィルタは指定してはいけません。二重に補間がかかることになります。

また、はみ出したときにテクスチャを循環するとそれもうまくいかなくなるのはわかると思います。

定数宣言

const float2 TexSize; //テクスチャサイズ
const float2 TexDiffU; //U方向の差分
static const float4 p1val = {   1.0,  1.0,  1.0,  1.0 };
static const float4 m1val = {  -1.0, -1.0, -1.0, -1.0 };
#define A (-1.0)
//定数係数宣言
static const float4 t1val = { A + 2.0, -(A + 3.0),	 0.0,	  1.0 }; //(|t| <= 1.0)での係数
static const float4 t2val = {	   A,   -5.0 * A, 8.0 * A, -4.0 * A }; //(1.0 < |t| <= 2.0)での係数
#undef A

p1valとm1valは計算中に使用する定数でそのままの意味です。

固定できない要素としてテクスチャサイズとU方向の差分があります。これらは以下のようにして計算します。

描画元テクスチャサイズが(w,h)のとき、

TexSize = { w, h }; TexDiffU = { 2.0 / w, 0.0 }

t1val,t2valはコメント通りの意味です。xyzwの順番にそれぞれt^3,t^2,t^1,t^0の係数です。

今は両方ともstatic constで指定していますが、Aを可変(つまり設定で変更可能)にするには、staticを削除してShaderの定数として与えてやるといいです。

PixelShader演算部

float4 PS(VS_OUTPUT In) : COLOR
{
	float4 clr0,clr1,clr2,clr3; float4 w1,w2; float4 tex1,tex2;
	tex1.xy = frac(In.Tex5 * TexSize);
	tex1.zw = tex1.xy + m1val.xy;
	tex2 = tex1 + p1val;
	w1 = t1val.x;			 w2 = t2val.x;
	w1 = w1 * tex1 + t1val.y; w2 = w2 * tex2 + t2val.y;
	w1 = w1 * tex1 + t1val.z; w2 = w2 * tex2 + t2val.z;
	w1 = w1 * tex1 + t1val.w; w2 = w2 * tex2 + t2val.w;
	
	clr0 = tex2D(Samp,In.Tex0) * w2.x;
	clr1 = tex2D(Samp,In.Tex1) * w2.x;
	clr2 = tex2D(Samp,In.Tex2) * w2.x;
	clr3 = tex2D(Samp,In.Tex3) * w2.x;
	clr0 += tex2D(Samp,In.Tex4) * w1.x;
	clr1 += tex2D(Samp,In.Tex5) * w1.x;
	clr2 += tex2D(Samp,In.Tex6) * w1.x;
	clr3 += tex2D(Samp,In.Tex7) * w1.x;
	clr0 += tex2D(Samp,In.Tex0 + TexDiffU) * w1.z;
	clr1 += tex2D(Samp,In.Tex1 + TexDiffU) * w1.z;
	clr2 += tex2D(Samp,In.Tex2 + TexDiffU) * w1.z;
	clr3 += tex2D(Samp,In.Tex3 + TexDiffU) * w1.z;
	clr0 += tex2D(Samp,In.Tex4 + TexDiffU) * w2.z;
	clr1 += tex2D(Samp,In.Tex5 + TexDiffU) * w2.z;
	clr2 += tex2D(Samp,In.Tex6 + TexDiffU) * w2.z;
	clr3 += tex2D(Samp,In.Tex7 + TexDiffU) * w2.z;
	return saturate(clr0 * w2.y + clr1 * w1.y + clr2 * w1.w + clr3 * w2.w);
}

重み係数を計算した上で各点をサンプリングしたときに重みを乗算して最後に足しあわせれば完了です。

なお、重み係数の特性(w(t)において、w(t) = 0 (tは0以外の整数)という性質)があるので、つなぎ目の部分に関してはそのまま行っても問題ありません。

また、重み係数を計算する前にすでに絶対値をとっているので、以降の計算では絶対値をとるような演算はありません。

使用するプログラム側の定義

ちょっと面倒ですが、以下のようなルーチンです。

//頂点情報
typedef struct _BicubicVertex{
	float fx,fy,fz,frhw;
	float tuv[16];
} BICUBICVERTEX;
//頂点のFVFの定義
#define BICUBIC_FVF	(D3DFVF_XYZRHW | D3DFVF_TEX8)
…
//頂点の作成処理
//vertexは矩形4点の位置情報を持っているとする(fx,fy,fu,fv)
//テクスチャのサイズを(w,h)とする
BICUBICVERTEX *lpVertex = (BICUBICVERTEX *)lpVertexBuffer;
du = 1.0f / (float)w; dv = 1.0f / (float)h;
for(i = 0;i < 4;i++,lpVertex++){
	lpVertex->fx = vertex[i].fx; lpVertex->fy = vertex[i].fy; lpVertex->fz = 0.0f; lpVertex->frhw = 1.0f;
	for(fu = vertex[i].fu,fv = vertex[i].fv,j = 0;j < 8;j++){
		lpVertex->tuv[2 * j + 0] = fu + (float)((j / 4) - 1) * du;
		lpVertex->tuv[2 * j + 1] = fv + (float)((j % 4) - 1) * dv;
	}
}
//Shader定数の計算
float fShaderConstValue[8];
memset(fShaderConstValue,0x00,sizeof(fShaderConstValue));
fShaderConstValue[0] = (float)w;
fShaderConstValue[1] = (float)h;
fShaderConstValue[4] = 2.0f * du;

という頂点情報を定義すれば、あとはPixelShaderの描画手順に従ってやってください。

なお、この三次補間はPixelShader2.0以上がなければ実行できません。ここ2,3年までのPCならほぼすべて対応しているはずですが・・・。

ちなみに、重み係数の補助係数aを-0.5としたときのみ重み計算を使わない特殊な計算が存在しますが、それはここでは解説する必要はないと思います。

次回はLanczos-3を使った補間処理をやってみたいと思います。


コメントを残す

メールアドレスが公開されることはありません。

*

この記事のトラックバック用URL