双3次補間サンプリング


~ Bi-cubic sampling ~






■はじめに

周りから「GPUでバイキュービックぐらいできるだろ」いわれたのでやりました。
一番右のテクスチャがバイキュービックサンプリングしたものです(書かれている文字に特に意味はありません)。

バイリニアが全体的にぼけるのに対して、バイキュービックは「しまりがある」ぼかしになります。
また、今回の方法では、45度のラインが綺麗な直線になります。

ただし、4角形などの角では、逆に内側が変にへこんだりします。

まぁ、これらは補間関数で調整が可能なので、特に気にしないで行きましょう。

で、いつものようにプログラムです。

ソースには、いつものように適当にファイルが入っています。 大事なファイルは次のものです。

main.hアプリケーションのヘッダ
main.cppアプリケーションのソース
hlsl.fxシェーダプログラム

■何をやっているの

普段、テクスチャをサンプリングする時には、ポイントサンプリングや線形サンプリングを使うのですが、 もう少し高度な方法として、3次曲線を使って周辺のテクセルの値から読み込むテクスチャの色を決定する方法が3次補間です。
テクスチャに使うときには、縦と横の2方向から合成をするので、「2」を意味する双(Bi)をつけて、双3次補間といいます。

普通は、双3次補間をするときには、テクスチャ座標の周囲16点のサンプリング点からテクセルを読み込んで、縦と横方向に3次補間を施します。

数式で表現すると、補間多項式をW、テクスチャ座標の左上のサンプリングポイントからの距離をu,vとして、各サンプリング点での色をcijとすれば、双3次補間サンプリングは次のようになります。

なお、uやvは、最近接テクセル間の距離が1になるような距離で正規化されます。
この補間関数Wは、一般的には任意です。 ただし、縦方向と横方向で同じ補間多項式を使うことに注目しましょう。
このような式の場合、和は2つの式

に分離することができます。
これは、実装時に、縦方向の補間を行った後に、横方向の(同じ係数の)補間を行えば最終的な値が得られるという意味で、押さえておきたい変形です(ガウスぼかしの分解と同じようなもんです)。
補間関数Wは、任意なのですが、今回は次のような補間多項式を使いました。

これは、Perlinノイズの勉強のときに見かけ多識なのですが、Catmull-Rom 曲線の端点での傾きが2倍になったものになっています。 最初は、Catmull-Rom をそのまま使ったのですが、普通に滑らかになってつまらなかったので、 このような曲線を採用しました(つまり、傾きを調整すれば、補間の強さの調整ができるということです)。

■シェーダプログラム

今回は、ピクセルシェーダだけの実装になっています。
シェーダプログラムは、主に3つの部分に分かれます。
1つは、合成する重みを計算する部分です。
最初に指定されたテクスチャ座標が、テクセル内のどのを指示しているのか調べます。 次に、それらの位置を3乗まですると共に、重みをつけて合成します。
この重みは、先ほどの式をしこしこと分解して求めます。

hlsl.fx
0034: float4 PS (VS_OUTPUT In) : COLOR
0035: {   
0043:     const float4 w3 = float4(-1, 1,-1, 1);
0044:     const float4 w2 = float4( 2,-2, 1,-1);
0045:     const float4 w1 = float4(-1, 0, 1, 0);
0046:     const float4 w0 = float4( 0, 1, 0, 0);
0048:     
0049:     float2 fUV = frac (In.Tex5 * vSize);    // サブテクセルを0~1のパラメータ化
0050:     float2 fUV2 = fUV * fUV;                // fUV の2乗
0051:     float2 fUV3 = fUV * fUV2;               // fUV の3乗
0052:     float4 u, v;
0053:     u = fUV3.x * w3 + fUV2.x * w2 + fUV.x * w1 + w0;
0054:     v = fUV3.y * w3 + fUV2.y * w2 + fUV.y * w1 + w0;

次に、テクスチャをサンプリングします。
高速化と、シェーダ命令の削減のために、テクスチャ座標は8つの座標をアプリケーション側で先に計算して、それらの位置でサンプリングすると共に、 残りの8点は一様にずらしてテクスチャ座標とします。
今回は、ポイントサンプリングを使って、半端な位置のテクスチャを読み込んでも、それなりの値が得られるようにしています。

hlsl.fx
0056:     // テクスチャから少しずらしてサンプリング
0057:     float4 col00 = tex2D( SrcSamp, In.Tex0 );
0058:     float4 col01 = tex2D( SrcSamp, In.Tex1 );
0059:     float4 col02 = tex2D( SrcSamp, In.Tex2 );
0060:     float4 col03 = tex2D( SrcSamp, In.Tex3 );
0061: 
0062:     float4 col10 = tex2D( SrcSamp, In.Tex4 );
0063:     float4 col11 = tex2D( SrcSamp, In.Tex5 );
0064:     float4 col12 = tex2D( SrcSamp, In.Tex6 );
0065:     float4 col13 = tex2D( SrcSamp, In.Tex7 );
0066: 
0067:     float4 col20 = tex2D( SrcSamp, In.Tex0 + d2u );
0068:     float4 col21 = tex2D( SrcSamp, In.Tex1 + d2u );
0069:     float4 col22 = tex2D( SrcSamp, In.Tex2 + d2u );
0070:     float4 col23 = tex2D( SrcSamp, In.Tex3 + d2u );
0071: 
0072:     float4 col30 = tex2D( SrcSamp, In.Tex4 + d2u );
0073:     float4 col31 = tex2D( SrcSamp, In.Tex5 + d2u );
0074:     float4 col32 = tex2D( SrcSamp, In.Tex6 + d2u );
0075:     float4 col33 = tex2D( SrcSamp, In.Tex7 + d2u );

最後に、計算した重みとサンプリングした色で合成します。
はじめに、横方向に合成してから、後で縦方向に合成します。

hlsl.fx
0078:     float4 col0 = u.x * col00 + u.y * col10 + u.z * col20 + u.w * col30;
0079:     float4 col1 = u.x * col01 + u.y * col11 + u.z * col21 + u.w * col31;
0080:     float4 col2 = u.x * col02 + u.y * col12 + u.z * col22 + u.w * col32;
0081:     float4 col3 = u.x * col03 + u.y * col13 + u.z * col23 + u.w * col33;
0082:     
0083:     return v.x * col0 + v.y * col1 + v.z * col2 + v.w * col3;
0084: }

■テクスチャ座標

アプリケーション側では、テクスチャ座標を指定して描画します。
シェーダプログラムに流し込むデータは、次のような8つのテクスチャ座標を持つ構造体です。

main.cpp
0040: typedef struct {
0041:     FLOAT       p[4];
0042:     FLOAT       tu0, tv0;
0043:     FLOAT       tu1, tv1;
0044:     FLOAT       tu2, tv2;
0045:     FLOAT       tu3, tv3;
0046:     FLOAT       tu4, tv4;
0047:     FLOAT       tu5, tv5;
0048:     FLOAT       tu6, tv6;
0049:     FLOAT       tu7, tv7;
0050: } TVERTEX8;

後は、べたに構造体にデータを流し込みます。
テクセル単位でずらす値として、テクセル幅の逆数(du,dv)を計算しておいて、各テクスチャ座標をテクセルごとにずらします。

main.cpp
0296:             TVERTEX8 Vertex8[4] = {
0297:                 // x      y    z rhw  uv0
0298:                 {x + 0, y + 0, 0, 1, 0-1.5f*du, 0-1.5f*dv,  0-1.5f*du, 0-0.5f*dv,  0-1.5f*du, 0+0.5f*dv,  0-1.5f*du, 0+1.5f*dv
0299:                                     ,0-0.5f*du, 0-1.5f*dv,  0-0.5f*du, 0-0.5f*dv,  0-0.5f*du, 0+0.5f*dv,  0-0.5f*du, 0+1.5f*dv},
0300:                 {x + w, y + 0, 0, 1, 1-1.5f*du, 0-1.5f*dv,  1-1.5f*du, 0-0.5f*dv,  1-1.5f*du, 0+0.5f*dv,  1-1.5f*du, 0+1.5f*dv
0301:                                     ,1-0.5f*du, 0-1.5f*dv,  1-0.5f*du, 0-0.5f*dv,  1-0.5f*du, 0+0.5f*dv,  1-0.5f*du, 0+1.5f*dv},
0302:                 {x + w, y + h, 0, 1, 1-1.5f*du, 1-1.5f*dv,  1-1.5f*du, 1-0.5f*dv,  1-1.5f*du, 1+0.5f*dv,  1-1.5f*du, 1+1.5f*dv
0303:                                     ,1-0.5f*du, 1-1.5f*dv,  1-0.5f*du, 1-0.5f*dv,  1-0.5f*du, 1+0.5f*dv,  1-0.5f*du, 1+1.5f*dv},
0304:                 {x + 0, y + h, 0, 1, 0-1.5f*du, 1-1.5f*dv,  0-1.5f*du, 1-0.5f*dv,  0-1.5f*du, 1+0.5f*dv,  0-1.5f*du, 1+1.5f*dv
0305:                                     ,0-0.5f*du, 1-1.5f*dv,  0-0.5f*du, 1-0.5f*dv,  0-0.5f*du, 1+0.5f*dv,  0-0.5f*du, 1+1.5f*dv},
0306:             };

後は、シェーダを設定して描画です。

main.cpp
0323:                 m_pd3dDevice->SetSamplerState( 0, D3DSAMP_MINFILTER, D3DTEXF_POINT );
0324:                 m_pd3dDevice->SetSamplerState( 0, D3DSAMP_MAGFILTER, D3DTEXF_POINT );
0325:                 m_pd3dDevice->SetFVF( D3DFVF_XYZRHW | D3DFVF_TEX8 );
0326:                 if( m_pEffect != NULL ) {
0327:                     m_pEffect->SetTechnique( m_hTechnique );
0328:                     m_pEffect->Begin( NULL, 0 );
0329:                     m_pEffect->Pass( 0 );
0330:                     m_pd3dDevice->DrawPrimitiveUP( D3DPT_TRIANGLEFAN, 2, Vertex8, sizeof( TVERTEX8 ) );
0331:                     m_pEffect->End();
0332:                 }

■最後に

欠点は、双3次補間をするだけで、他にはほとんど何もできなくなることでしょうか。
よっぽど場所を区切れば使えるかもしれませんね。





もどる

imagire@gmail.com