GeForce FX 用の、オリジナルのネタを考えてみました。
上の図の、左側が元の絵で、右側が中央値フィルタを掛けた画像です。
中央値フィルタは、点々としたポイントノイズを除去する効果があります(完全にではありませんが)。
下のファイルがソースと実行ファイルです。
まぁ、いつものように適当にファイルが入っています。
今回は、天球を半球にしてみました。
fp.cg | フラグメントプログラム。 |
main.cpp | メインループ。 |
CBitmap.h | ビットマップファイルの読み込み。 |
CBitmap.cpp | ビットマップファイルの読み込み。 |
あと、実行ファイル及び、プロジェクトファイルが入っています。
中央値フィルタとは、局所的な領域での濃淡の中央の値を持つ色を出力するフィルタです。
特徴としては、一番上の絵のようなごま塩状のノイズを取り除くことができます。 ただし、完璧に取り除けるのは白か黒のノイズの時のみで、ノイズの色が中間の色の時には、その色が残ります。 例えば、下の絵では一部のノイズが残ってしまいました。
また、比較的、元の形を残すフィルタです。
単純なサンプリングではなく、真中の値を決める計算が必要とのことで、 いままでのシェーダではできませんでした。
今回は、ビットマップファイルを読み込んで、テクスチャとして張り込んでいます。
ビットマップファイルの読み込みをどうしようかとネットで探したら、
sonsonさんのサイトにそのままずばりのプログラムがあったので、そのプログラムを参考にしました。
読み込みの手順は次のようになります。
基本的には、ファイルをオープンしてヘッダなどを読み込んだ後に、フォーマット等をチェックして
データ種別ごとに読み込みます。
私は基本的に「goto大好きっ娘」なので、エラーが起きた後は最後の行までジャンプして処理するようにしています。
別にgotoなど使わなくても書けるのですが、このほうがバグがでないと思いませんか?
CBitmap.cpp 0161: unsigned int CBitmap::Load(const char *filename) 0162: { 0163: unsigned int tex_id = 0; 0164: FILE *fp; 0165: //ビットマップヘッダ 0166: BitmapHeader header; 0167: BitmapInfoHeader info; 0168: //色数 0169: unsigned int color_bit; 0170: 0171: // ファイルオープン 0172: if( NULL==( fp = fopen(filename, "rb") ) ) return tex_id; 0173: 0174: //ヘッダ読み込み 0175: if( _ReadHeader(&header, fp)) goto end; 0176: if( _ReadInfoHeader(&info, fp)) goto end; 0177: 0178: //ヘッダチェック 0179: if( !(color_bit = _CheckHeaders(&header, &info))) goto end; 0180: 0181: //ビットマップファイルサイズチェック 0182: if( !_CheckSize(info.width*info.height) ) goto end; 0183: 0184: //カラービット数で処理を分ける 0185: switch(color_bit){ 0186: case 8: 0187: tex_id = _Analize8bit(&info, fp); 0188: break; 0189: case 24: 0190: tex_id = _Analize24bit(&info, fp); 0191: break; 0192: } 0193: 0194: end: 0195: // ファイルクローズ 0196: fclose(fp); 0197: 0198: return tex_id; 0199: }
さて、フォーマットのチェックで何をするかということですが、
「ビットマップファイルの先頭は「BM」で始まっている」等の規約がありますので、
それらをチェックします。
この辺は、仕様で決まっているので、さらに深く知りたい方は、フォーマットの仕様をチェックしましょう。
CBitmap.cpp 0006: // --------------------------------------------------------------------------- 0007: // Bitmapヘッダチェック 0008: // --------------------------------------------------------------------------- 0009: int CBitmap::_CheckHeaders (const BitmapHeader *pBH, const BitmapInfoHeader *pBIH) 0010: { 0011: //ヘッダのBMの文字 0012: if( pBH->distinct1!='B' || pBH->distinct2!='M' )return 0; 0013: //レイヤー1 0014: if( pBIH->plane !=1 )return 0; 0015: //圧縮なし 0016: if( pBIH->compression !=0 ) return 0; 0017: //色数を返す 0018: if( pBIH->bits ==24 || pBIH->bits ==8 ) return pBIH->bits; 0019: 0020: return 0; 0021: }
今回は、無圧縮256色か24bit色のファイルだけを取り扱います。
あと、OpenGLの仕様として、「テクスチャの幅xテクスチャの高さ=2^n」である必要があるので、
このチェックもします。
ということで、読み込めるテクスチャにはこの制限が付きますので注意してください。
CBitmap.cpp 0022: // --------------------------------------------------------------------------- 0023: // ビットマップファイルのサイズチェック 0024: // --------------------------------------------------------------------------- 0025: int CBitmap::_CheckSize(unsigned int pict_size) 0026: { 0027: unsigned int i=2; 0028: const unsigned int TEX_SIZE = 256; 0029: unsigned int max_texture = TEX_SIZE*TEX_SIZE; 0030: 0031: //サイズチェック 0032: for(i=2;i<=max_texture;i*=2){ 0033: if( i == pict_size){ 0034: return 1; 0035: } 0036: } 0037: return 0; 0038: };
フォーマットが分かったら実際にデータを読み込んで、テクスチャとして設定します。
24bitsの時は中身がべたで入っているので、そのまま読み込んでイメージとしてからテクスチャにします。
但し、データの中身の順序が逆なので読み込むときに御尻から読み込む必要があります。
CBitmap.cpp 0124: // --------------------------------------------------------------------------- 0125: // 24bitsテクスチャの読み込み 0126: // --------------------------------------------------------------------------- 0127: unsigned int CBitmap::_Analize24bit( const BitmapInfoHeader *pBIH, FILE *fp ) 0128: { 0129: unsigned int tex_id = 0; 0130: unsigned char *bitdata; 0131: unsigned char red,green,blue; 0132: int i, j; 0133: 0134: //メモリ確保 0135: bitdata = (unsigned char*)malloc(3*pBIH->width*pBIH->height*sizeof(unsigned char)); 0136: //読み込み 0137: for(j=(pBIH->height-1);j>=0;j--){ 0138: for(i=(pBIH->width-1);i>=0;i--){ 0139: fread( &red, sizeof(unsigned char),1,fp); 0140: fread( &green, sizeof(unsigned char),1,fp); 0141: fread( &blue, sizeof(unsigned char),1,fp); 0142: *(bitdata+3*i+3*j*pBIH->width) = blue; 0143: *(bitdata+3*i+3*j*pBIH->width+1)= green; 0144: *(bitdata+3*i+3*j*pBIH->width+2)= red; 0145: } 0146: } 0147: //テクスチャ作成 0148: glGenTextures(1, &tex_id); 0149: glBindTexture(GL_TEXTURE_2D, tex_id); 0150: glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, pBIH->width, pBIH->height, 0151: 0, GL_RGB, GL_UNSIGNED_BYTE, bitdata); 0152: 0153: //メモリ開放 0154: free(bitdata); 0155: 0156: return tex_id; 0157: }
テクスチャを読み込むときは、ロードして、適当な属性を設定します。
main.cpp
0167: //テクスチャの生成
0168: tex = CBitmap::Load("test.bmp");
0169: glBindTexture(GL_TEXTURE_2D, tex);
0170: glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
0171: glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
0172: glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
0173: glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
0174: glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);
以上でテクスチャを読み込むことができます。
この辺は、D3DXのような標準のペルパーライブラリを作ってもらいたいものです。
さて、肝心かなめのフラグメントプログラムです。
今回は頂点プログラムはありません。
入力された頂点が座標変換されて、フラグメントプログラムへデータが入力されます。
fp.cg 0001: // --------------------------------------------------------------------------- 0002: // 頂点プログラム出力データ 0003: // --------------------------------------------------------------------------- 0004: struct vert2frag 0005: { 0006: float4 hpos : HPOS; // 頂点座標 0007: float4 tcoords : TEX0; // テクスチャ座標 0008: };
今回入力されるデータはテクスチャ座標です。
ポリゴンにあわせてテクスチャが張られるように座標を調整します。
フラグメントプログラムでは、最初にテクセル中心とその周りの4テクセルの色をサンプリングします。
GeForce FX 世代では、1つのテクスチャから5回サンプリングするなんてできるので、
テクスチャ座標を少しずつずらしながら、読み込みます。
中心の座標はテクセル中心に合わせるために、(0.5, 0.5)にしています。
fp.cg 0009: // --------------------------------------------------------------------------- 0010: // Median filter フラグメントプログラム 0011: // --------------------------------------------------------------------------- 0012: fragout main(vert2frag I 0013: , uniform texobj2D SrcMap : texunit0 0014: ) 0015: { 0016: fragout O; 0017: 0020: float2 shift00 = { 0.5f/256.0f, 0.5f/256.0f}; // 上下左右中心5つからサンプリング 0021: float2 shift01 = { 0.5f/256.0f, 1.5f/256.0f}; // 00 01 10 0N N0 0022: float2 shift10 = { 1.5f/256.0f, 0.5f/256.0f}; // x x x o x 0023: float2 shift0N = { 0.5f/256.0f,-0.5f/256.0f}; // xox xxx xxo xxx oxx 0024: float2 shiftN0 = {-0.5f/256.0f, 0.5f/256.0f}; // x o x x x 0025: 0026: // テクスチャから少しずらしてサンプリング 0027: float3 col0 = f3tex2D( SrcMap, I.tcoords.xy + shift00 ); 0028: float3 col1 = f3tex2D( SrcMap, I.tcoords.xy + shift01 ); 0029: float3 col2 = f3tex2D( SrcMap, I.tcoords.xy + shift10 ); 0030: float3 col3 = f3tex2D( SrcMap, I.tcoords.xy + shift0N ); 0031: float3 col4 = f3tex2D( SrcMap, I.tcoords.xy + shiftN0 );
次に、テクセルの色の強さを求めます。
色の強さは、内積計算だけで求められます。
fp.cg 0018: float3 rgb2lum = {0.299, 0.587, 0.114}; // RGB から、色の強さを求める定数 0032: 0033: // 色の強さを求める 0034: float b0 = dot( col0, rgb2lum ); 0035: float b1 = dot( col1, rgb2lum ); 0036: float b2 = dot( col2, rgb2lum ); 0037: float b3 = dot( col3, rgb2lum ); 0038: float b4 = dot( col4, rgb2lum );
さて、今得られた色の強さの真ん中の値をピクセルの色にします。
この「真ん中」を得る方法が問題です。
フラグメントプログラムでは、さすがにソートは使えないので、
別の方法を考えなくてはなりません。
今回行う方法は、各テクセルの色の強さが上から数えて何番目かを数えて、
3番目に強い色をピクセルの色に採用します。
色の順番 flag* が2の時に3番目の色になりますが、
浮動小数点数計算をするので厳密に「2」の値を見れないので、
±0.5の範囲を調べて「2」の値を持つテクセルがどれか調べます。
また、色が同じ時にも優先をつけるために、比較する時に等号をどちらか付けて「2」の値が2つ以上でないように気をつけました
(左辺の番号が右辺の番号よりも大きいときに等号を付けました)。
fp.cg 0040: // 色の順番を求める(「4-値」の並びの位置にいる) 0041: float flag0 = ((b0< b1)?1.0:0.0) + ((b0< b2)?1.0:0.0) + ((b0< b3)?1.0:0.0) + ((b0< b4)?1.0:0.0); 0042: float flag1 = ((b1<=b0)?1.0:0.0) + ((b1< b2)?1.0:0.0) + ((b1< b3)?1.0:0.0) + ((b1< b4)?1.0:0.0); 0043: float flag2 = ((b2<=b0)?1.0:0.0) + ((b2<=b1)?1.0:0.0) + ((b2< b3)?1.0:0.0) + ((b2< b4)?1.0:0.0); 0044: float flag3 = ((b3<=b0)?1.0:0.0) + ((b3<=b1)?1.0:0.0) + ((b3<=b2)?1.0:0.0) + ((b3< b4)?1.0:0.0); 0045: 0046: // 中心の色をメディアンとして採用 0047: O.col.xyz = ( 1.5f<flag0 && flag0<2.5f ) ? col0 : 0048: (( 1.5f<flag1 && flag1<2.5f ) ? col1 : 0049: (( 1.5f<flag2 && flag2<2.5f ) ? col2 : 0050: (( 1.5f<flag3 && flag3<2.5f ) ? col3 : col4 ))); 0051: 0052: return O; 0053: }
某suzunaさんとの飲み会のときにミキタさんから、
「Cg使い物になるん?」聞かれたので、ためしに今回のアセンブラを表示してみると下のようになります。
ざっと112命令あります。こんなのを手で書く気にはなれません。
これだけでも非常に有用であることがわかるかと思います。
欠点としては、リソースが豊富にあるので最適化をあまり考えなくなってしまうことですね。
今回も、比較処理の部分等でアルゴリズム的により軽い処理も考えれられるのですが、命令数が目では見えないので、どうもやる気がしなくなります。
!!FP1.0 # NV_fragment_program generated by NVIDIA Cg compiler # cgc version 1.5.0001, build date Sep 12 2002 15:50:03 # command line args: fp.cg -o C:\cgt168A.tmp -l C:\cgt168B.tmp -q -profile fp30 #vendor NVIDIA Corporation #version 1.0.1 #profile fp30 #program main #semantic main.SrcMap : texunit0 #var float4 I.tcoords : $vin.TEX0 : TEX0 : 0 : 1 #var texobj2D SrcMap : texunit0 : texunit 0 : 1 : 1 #var half4 col : $vout.COL : COL : -1 : 1 #var float depth : $vout.DEPR : DEPR : -1 : 1 ADDR R0.xy, f[TEX0].xyyy, {-0.001953125, 0.001953125}.xyyy; TEX R0.xyz, R0.xyyy, TEX0, 2D; ADDR R1.xy, f[TEX0].xyyy, {0.001953125, -0.001953125}.xyyy; TEX R9.xyz, R1.xyyy, TEX0, 2D; MOVR R11.x, {0}.x; DP3R R4.x, R0.xyzz, {0.29899999, 0.58700001, 0.114}.xyzz; DP3R R5.x, R9.xyzz, {0.29899999, 0.58700001, 0.114}.xyzz; SLTR H2.x, R5.x, R4.x; MOVXC HC.x, H2.x; MOVR R11.x(GT.x), {1}.x; MOVR R12.x, {0}.x; ADDR R1.xy, f[TEX0].xyyy, {0.005859375, 0.001953125}.xyyy; TEX R8.xyz, R1.xyyy, TEX0, 2D; DP3R R2.x, R8.xyzz, {0.29899999, 0.58700001, 0.114}.xyzz; SLER H2.x, R5.x, R2.x; MOVXC HC.x, H2.x; MOVR R12.x(GT.x), {1}.x; MOVR R13.x, {0}.x; ADDR R1.xy, f[TEX0].xyyy, {0.001953125, 0.005859375}.xyyy; TEX R3.xyz, R1.xyyy, TEX0, 2D; DP3R R7.x, R3.xyzz, {0.29899999, 0.58700001, 0.114}.xyzz; SLER H2.x, R5.x, R7.x; MOVXC HC.x, H2.x; MOVR R13.x(GT.x), {1}.x; MOVR R10.x, {0}.x; ADDR R1.xy, f[TEX0].xyyy, {0.001953125, 0.001953125}.xyyy; TEX R1.xyz, R1.xyyy, TEX0, 2D; DP3R R6.x, R1.xyzz, {0.29899999, 0.58700001, 0.114}.xyzz; SLER H28.x, R5.x, R6.x; MOVXC HC.x, H28.x; MOVR R10.x(GT.x), {1}.x; ADDR R10.x, R10.x, R13.x; ADDR R10.x, R10.x, R12.x; ADDR R11.x, R10.x, R11.x; SLTR H21.x, R11.x, {2.5}.x; SLTR H20.x, {1.5}.x, R11.x; MULX H20.x, H20.x, H21.x; MOVXC HC.x, H20.x; MOVR R0.xyz(GT.xxxx), R9.xyzz; MOVR R10.x, {0}.x; SLTR H18.x, R2.x, R4.x; MOVXC HC.x, H18.x; MOVR R10.x(GT.x), {1}.x; MOVR R11.x, {0}.x; SLTR H18.x, R2.x, R5.x; MOVXC HC.x, H18.x; MOVR R11.x(GT.x), {1}.x; MOVR R12.x, {0}.x; SLER H18.x, R2.x, R7.x; MOVXC HC.x, H18.x; MOVR R12.x(GT.x), {1}.x; MOVR R9.x, {0}.x; SLER H26.x, R2.x, R6.x; MOVXC HC.x, H26.x; MOVR R9.x(GT.x), {1}.x; ADDR R9.x, R9.x, R12.x; ADDR R9.x, R9.x, R11.x; ADDR R10.x, R9.x, R10.x; SLTR H19.x, R10.x, {2.5}.x; SLTR H18.x, {1.5}.x, R10.x; MULX H18.x, H18.x, H19.x; MOVXC HC.x, H18.x; MOVR R0.xyz(GT.xxxx), R8.xyzz; MOVR R9.x, {0}.x; SLTR H16.x, R7.x, R4.x; MOVXC HC.x, H16.x; MOVR R9.x(GT.x), {1}.x; MOVR R10.x, {0}.x; SLTR H16.x, R7.x, R5.x; MOVXC HC.x, H16.x; MOVR R10.x(GT.x), {1}.x; MOVR R11.x, {0}.x; SLTR H16.x, R7.x, R2.x; MOVXC HC.x, H16.x; MOVR R11.x(GT.x), {1}.x; MOVR R8.x, {0}.x; SLER H24.x, R7.x, R6.x; MOVXC HC.x, H24.x; MOVR R8.x(GT.x), {1}.x; ADDR R8.x, R8.x, R11.x; ADDR R8.x, R8.x, R10.x; ADDR R9.x, R8.x, R9.x; SLTR H17.x, R9.x, {2.5}.x; SLTR H16.x, {1.5}.x, R9.x; MULX H16.x, H16.x, H17.x; MOVXC HC.x, H16.x; MOVR R0.xyz(GT.xxxx), R3.xyzz; MOVR R3.x, {0}.x; SLTR H8.x, R6.x, R4.x; MOVXC HC.x, H8.x; MOVR R3.x(GT.x), {1}.x; MOVR R4.x, {0}.x; SLTR H10.x, R6.x, R5.x; MOVXC HC.x, H10.x; MOVR R4.x(GT.x), {1}.x; MOVR R5.x, {0}.x; SLTR H4.x, R6.x, R2.x; MOVXC HC.x, H4.x; MOVR R5.x(GT.x), {1}.x; MOVR R2.x, {0}.x; SLTR H12.x, R6.x, R7.x; MOVXC HC.x, H12.x; MOVR R2.x(GT.x), {1}.x; ADDR R2.x, R2.x, R5.x; ADDR R2.x, R2.x, R4.x; ADDR R3.x, R2.x, R3.x; SLTR H5.x, R3.x, {2.5}.x; SLTR H4.x, {1.5}.x, R3.x; MULX H4.x, H4.x, H5.x; MOVXC HC.x, H4.x; MOVR R0.xyz(GT.xxxx), R1.xyzz; MOVH o[COLH].xyz, R0.xyzz; END
メディアンフィルタは、ノイズリダクションフィルタなので、
フォトンマップの画像に使えるかなぁと考えていました。
ためしに、フォトンマップの画像にかけると、次のようになります(^c さん、勝手に使ってごめんなさい)。
同じく右がフィルタを掛けた画像です。
あまり綺麗にノイズが取れなかったので、ちょっと残念でした。
まぁ、それ以前に、Bee さんに「わざわざフォトントレースをしたのに、
フィルタをかけて誤魔化すのは本末転倒だ」といわれたことがあるので、
これ以上、このネタには言及するのは止めましょう。
今回は4近傍と中心のピクセルを取ったのですが、 8近傍を考えるとよりノイズを取る効果が出ると思います。 ところが、そのまま組むと、シェーダの命令数だかレジスタ数だかをオーバーしてしまうようで、 今回は断念しました。