イメージシンセシス トップページ

GLSLの演習:テクスチャの利用

■ テクスチャマッピング

上の「図:ポリゴンへのテクスチャマッピング」に示されているように,テクスチャはテクスチャ空間上で定義される. この空間はテクスチャ座標 [ s, t ] で定義され,その範囲は 0 ≤ s ≤ 1,0 ≤ t ≤ 1 である. ポリゴンにテクスチャをマッピングする場合,ポリゴンの各頂点にテクスチャ座標値を割り当てる.

■ 準備

サンプルプログラム glsl1 に次のサンプルプログラムを加える.

サンプルプログラム(テクスチャの基本的な利用) (Visual Studio 2010 で作成)
  1. 上の zip ファイルを解凍したフォルダに含まれる全てのファイルをフォルダ glsl1 中に加える.
  2. サンプルプログラム glsl1 を起動した状態で,main 関数を含むソースファイル main.cpp をプロジェクトから除外し(削除はしない),新たな main 関数を含むソースファイル main_tex.cpp をプロジェクトに追加する.
  3. バーテックスシェーダのソースファイル tex.vert とフラグメントシェーダのソースファイル tex.frag をプロジェクトに追加する.

■ 演習

以下,順次,プログラム中の赤字の箇所を変更して実行する. 青字は,その時点で,すでに変更済みの箇所である.

(1) テクスチャの基本的な利用

(1−1) OpenGLアプリケーションのみを利用したテクスチャマッピング

サンプルプログラムをそのまま実行する. OpenGLアプリケーションのみを利用したテクスチャマッピングが実行される.
このプログラムでは,OpenGLアプリケーション(main_tex.cpp)内の関数 main において,GLSL の初期設定と終了設定を行う関数 init_glsl,fin_glsl がコメントアウトされているため,GLSL によるシェーダ(tex.vert,tex.frag)は利用されず,OpenGLアプリケーション(*.cpp)のみが利用される.

このプログラムでは,画像をテクスチャにして4角形ポリゴンにマッピングし,4角形ポリゴンがぴったりとウィンドウ内に収まるように平行投影で表示している. 画像をテクスチャとする処理は,テクスチャの初期設定と終了設定を行う関数 init_tex と fin_tex で行っている. 4角形ポリゴンの定義,ならびに,テクスチャのマッピングについては,main_tex.cpp 内の関数 display 中で以下の処理を行っている. このプログラムの実行により,ウィンドウ内に次の画像が表示される.

(1―2) シェーダを利用したテクスチャマッピング

サンプルプログラムを以下のように変更して実行する. GLSL によるシェーダを利用したテクスチャマッピングが実行される. OpenGLアプリケーション(main_tex.cpp)内の関数 main において,GLSL の初期設定と終了設定を行う関数 init_glsl,fin_glsl を有効にしたため,GLSL によるシェーダ(tex.vert,tex.frag)が利用される.

このプログラムでは,main_tex.cpp 内の関数 display 中での関数 glTexCoord2d によるテクスチャ座標値の割り当てを無効にし,その割り当てをフラグメントシェーダ(tex.frag)内で以下のように行っている. このプログラムの実行により,(1−1)の場合と同様,ウィンドウ内に次の画像が表示される.

(1−3) 4角形ポリゴンにテクスチャマッピングしていることの確認

上の(1−1)と(1−2)のそれぞれの場合について,プログラムに次の処理(4角形ポリゴンに対するモデリング変換)を追加して実行し,4角形ポリゴンにテクスチャマッピングが行われていることを確認する. その際,(1−1)と(1−2)の場合で表示される結果が異なる理由を考えてみる. このプログラムの実行により,(1−1)と(1−2)のそれぞれの場合について,ウィンドウ内に次の画像が表示される.

元の画像の表示
モデリング変換なし
(1−1)の場合
OpenGLアプリケーションのみを利用
(1−2)の場合
シェーダを利用

このモデリング変換は,元の4角形ポリゴンに対して,関数 glScaled により x,y 方向のそれぞれで半分の大きさに縮小し,関数 glRotated により z 軸回り,つまり,x-y 平面内で左回りに 30 度だけ回転し,さらに,x 方向に imgx/3,y 方向に imgy/6 だけ平行移動している. その結果,元々はウィンドウ内にぴったりと収まっていた4角形ポリゴンが,上のように縮小されて斜めになって表示される. そして,(1−1)と(1−2)のそれぞれの場合で,4角形ポリゴンにテクスチャとしてマッピングされている画像の表示が異なっている.

(1−1)の場合,main_tex.cpp 内の関数 display 中で,関数 glTexCoord2d によって4角形ポリゴンの4個の頂点にテクスチャ(画像)の四隅のテクスチャ座標値を割り当てている. そのため,4角形ポリゴンの全面にテクスチャ(画像)全体がマッピングされる.

(1−2)の場合,tex.frag 内において,4角形ポリゴン内の各フラグメント(ピクセル)に対して,ウィンドウ内のフラグメント位置をあらわすフラグメント座標値 gl_FragCoord によってテクスチャ座標値が与えられる. 実際に表示されるのは4角形ポリゴンなので,結果として,ウィンドウ内にぴったりと収めたテクスチャ(画像)から4角形ポリゴンの領域だけが抜き出されて表示されることになる.

(1−4) フラグメント(ピクセル)座標値とモデリング座標値の違い

上の(1−3)で行った(1−2)の場合,すなわち,GLSL によるシェーダを利用した場合には,フラグメントシェーダ(tex.frag)においてフラグメント(ピクセル)座標値 gl_FragCoord を用いてテクスチャ座標値 tex_st を計算していた. そこで,その計算方法について,以下のようにモデリング座標値 gl_Vertex を用いるように変更し,プログラムを実行してみる. なお,OpenGLアプリケーション(main_tex.cpp)は(1−3)で行った(1−2)の場合と同じ状態にすること. このプログラムの実行によってウィンドウ内に表示される画像を(1−3)の場合と比較したものを次に示す.

元の画像の表示
モデリング変換なし
(1−3)の(1−1)の場合
OpenGLアプリケーションのみを利用
(1−3)の(1−2)の場合
フラグメント座標値 gl_FragCoord を利用
(1−4)の場合
モデリング座標値 gl_Vertex を利用

このプログラムでは,シェーダ(tex.vert,tex.frag)において,以下の処理を行っている.
  1. バーテックスシェーダ(tex.vert)とフラグメントシェーダ(tex.frag)で共通の varying 変数 vert_xy を定義する.

  2. バーテックスシェーダ(tex.vert)において,4角形ポリゴンの頂点のモデリング座標 gl_Vertex の x, y 値を varying 変数 vert_xy に格納してフラグメントシェーダ(tex.frag)に渡す.

  3. フラグメントシェーダ(tex.frag)において,(ラスタライザで補間された)4角形ポリゴンのモデリング座標の x, y 値を varying 変数 vert_xy で受け取り,それを用いてテクスチャ座標値 tex_st を計算する.
4角形ポリゴンの4個の頂点のモデリング座標値 gl_Vertex からテクスチャ座標値を求めているため,結果として,(1−3)の(1−1)の場合と同じ画像がウィンドウ内に表示される.

(2) シェーダの利用による処理の高速化

グラフィックス表示は負荷が大きな処理であり,CPU (Central Processing Unit) の負荷を軽減するため,通常,グラフィックス表示に特化した GPU (Graphics Processing Unit) という装置によって高速に実行される. シェーダによるプログラムは,ユーザが意図したグラフィックス表示の処理を GPU 上で高速に実行することを可能とする. GPU は,頂点ごと,あるいは,フラグメント(ピクセル)ごとの処理を並列に実行する仕組みによって高速な処理を可能としている. なお,理論的には,すべての頂点ごと,フラグメント(ピクセル)ごとの処理が並列に実行されるが,実際の並列の度合いは GPU のハードウェアの仕様に依存する.

GPU は元々はグラフィックス表示に特化した装置であるが,負荷が大きな処理を高速に実行できるという利点から,グラフィックス表示以外の一般的な処理,すなわち,通常は CPU 上で実行される処理を意図的に GPU 上で実行させることで,GPU の高速な処理能力を汎用的に利用することができる.このような GPU の利用は GPGPU (General-Purpose computing on Graphics Processing Units) と呼ばれる.

ここでは,GPGPU の観点から,GPU による高速な処理を確認するため,意図的にピクセルごとに大きな計算時間を要する処理(繰り返し処理)を行うプログラムを実行してみる. 計算量の多い処理を CPU 上で実行する場合GPU 上で実行する場合について,処理速度の違いを確認する.

なお,以下の実験のため,まず,プログラムを(1−1)の状態に戻す.

(2−1) OpenGLアプリケーションのみを利用した場合

以下のプログラムでは,計算量の多い処理を CPU 上で実行する. サンプルプログラムを以下のように変更して実行する. このプログラムは,OpenGLアプリケーション(main_tex.cpp)のみを利用して,元のカラー画像を簡易的な方法でグレースケール画像に変換する. 関数 main に追加した処理は,Nx × Ny = 640 × 480 個のピクセルを持つカラー画像について,すべてのピクセル座標値 x = 0 〜 Nx-1,y = 0 〜 Ny-1 に対する2重ループであり,ループ内ではピクセル [ x, y ] について以下の処理を行っている.
  1. 元のカラー画像を読み込んだ配列 img 中でピクセル [ x, y ] が持つ R,G,B 値を平均化したグレースケール値を変数 avg に代入する. 配列 img は GLubyte 型,すなわち,unsigned char 型であり,R,G,B のそれぞれの色が 0 〜 255 の範囲の値を持つ. 一方,(2−2)で比較するプログラムのフラグメントシェーダ(tex.frag)内では,色の値は float 型であり,0.0 〜 1.0 の範囲となる. そこで,条件を同じにするため,配列 img の値を 255 で除算している. なお,この追加した処理では,次の num 回の加算の処理が全体の処理時間にとって支配的であるため,この除算の影響は処理時間の点では無視できる.

  2. 変数 sum にグレースケール値 avg を num = 10000 回だけ加算する. この加算の処理は,カラー画像をグレースケール画像に変換する目的にとっては無意味であるが,(2−2)との比較により,シェーダの利用によって処理の高速化が実現できることを確認するために行う.

  3. 変数 sum の値を加算回数 num で除算して元のグレースケール値に戻し,さらに 255 を乗算したものをピクセル [ x, y ] が持つ R,G,B 値として配列 img に代入する.

(2−2) シェーダを利用した場合

以下のプログラムでは,(2−1)と同じ計算量の多い処理を GPU 上で実行する. サンプルプログラムを以下のように変更して実行する. このプログラムは,(2−1)のプログラムと実質的に同じ処理を行っている. ただし,(2−1)ではOpenGLアプリケーション(main_tex.cpp)内の2重ループでピクセル [ x, y ] ごとの処理を行っていたが,(2−2)では同じ処理をフラグメントシェーダ(tex.frag)内で行う. フラグメントシェーダ内の処理は,ラスタライザによる補間で生成されたすべてのフラグメント(ピクセル)に対して実行されるため,フラグメントシェーダ内で2重ループにする必要はない. このプログラムでは,tex.frag 内で,R,G,B 値からグレースケール値への変換,ならびに,グレースケール値の num = 10000 回の加算が行われる.

(2−1)と(2−2)のプログラムの実行により,ウィンドウ内に次のように同じ画像が表示される.

元のカラー画像
(2−1)
OpenGLアプリケーションのみを利用
(2−2)
シェーダを利用

しかし,それぞれのプログラムの実行により,すべてのピクセル [ x, y ] のグレースケール値を num = 10000 回だけ加算するという計算量の多い処理を CPU 上で実行する(2−1)と GPU 上で実行する(2−2)では,処理時間に大きな差があることが確認できる. (2−1)では,OpenGLアプリケーション(main_tex.cpp)内の Nx × Ny = 640 × 480 回の2重ループの処理が CPU 上で実行される. 一方,(2−2)では,フラグメントシェーダ(tex.frag)による GPU の利用によって,この処理がフラグメント(ピクセル)ごとに並列に実行され,高速化がなされる.

この実験では,一つの例として単純な(本質的には意味のない)加算の繰り返しを行ったが,一般に,通常は CPU 上で実行される処理をバーテックスシェーダによる頂点ごとの処理,あるいは,フラグメントシェーダによるフラグメント(ピクセル)ごとの処理にうまく置き換えることができれば,GPU の処理能力を利用して高速に実行することができる.

(3) シェーダを利用した画像処理

上で用いたサンプルプログラムを利用して,以下の画像処理を行うプログラムを作成する.

(3−1) グレースケール変換

フラグメントプログラム tex.frag を変更して,NTSC 加重平均法により元のカラー画像をグレースケール画像に変換するプログラムを作成する.

(3−2) カラー値の反転

フラグメントプログラム tex.frag を変更して,元のカラー画像の各ピクセルの R, G, B 値を反転するプログラムを作成する.

(3−3) 平滑化フィルタ

フラグメントプログラム tex.frag を変更して,元のカラー画像を平滑化するプログラムを作成する. 具体的には,各ピクセル P に対して,自分を中心とする ( 2 × M + 1 ) × ( 2 × M + 1 ) 個のピクセルが持つ R, G, B 値の平均値を与える. 異なる幾つかの M の値について,以下の実験を行い,結果の違いを確認する.
  1. ( 2 × M + 1 ) × ( 2 × M + 1 ) 個のピクセルが持つ R, G, B 値を単純に平均化する.

  2. ( 2 × M + 1 ) × ( 2 × M + 1 ) 個のピクセルに異なる重みを与えて加重平均化する. 例えば,以下のような重みを与える.

    3 × 3 ピクセル ( M = 1 )
    1/101/101/10
    1/102/101/10
    1/101/101/10
     
    5 × 5 ピクセル ( M = 2 )
    1/652/653/652/651/65
    2/653/654/653/652/65
    3/654/655/654/653/65
    2/653/654/653/652/65
    1/652/653/652/651/65

(3−4) エッジ検出フィルタ

フラグメントプログラム tex.frag を変更して,元のカラー画像のエッジ検出を行うプログラムを作成する. 具体的には,ピクセル座標値 [ x, y ] のピクセル P( x, y ) について,以下の処理を行う.
  1. ピクセル P( x, y ) を中心とする 3 × 3 個のピクセル P( xi, yj ) について,(3−1)のグレースケール変換を参考にして,グレースケール値 Y( xi, yj ) を計算する.

  2. 計算した 3 × 3 個のグレースケール値 Y( xi, yj ) に対して,x 方向と y 方向の微分フィルタを適用し,ピクセル P( x, y ) における微分値 Yx( x, y ) と Yy( x, y ) を求める. 微分フィルタとして下の2種類を用い,(3−3)と同様の方法で 3 × 3 個の Y( xi, yj ) を加重平均化した値を微分値とする. なお,下の図では,横方向を x 方向,縦方向を y 方向とする.

    A. プリウィット・フィルタ
    -10+1
    -10+1
    -10+1
    x 方向の微分
     
    -1-1-1
    0 0 0
    +1+1+1
    y 方向の微分

    B. ソーベル・フィルタ
    -10+1
    -20+2
    -10+1
    x 方向の微分
     
    -1-2-1
    0 0 0
    +1+2+1
    y 方向の微分

  3. 微分ベクトル [ Yx( x, y ), Yy( x, y ) ] から勾配値 G( x, y ) を求め,その値をエッジ検出の結果として,ピクセル P( x, y ) の色として出力する(変数 gl_FragColor の r, g, b の全てに代入する). 勾配値 G( x, y ) を求める方法として,次の3種類を用いる.

    1. 2つの微分値の絶対値のうちの大きなほう

      G( x, y ) = max ( | Yx( x, y ) |, | Yy( x, y ) | )

    2. 2つの微分値の絶対値の和

      G( x, y ) = | Yx( x, y ) | + | Yy( x, y ) |

    3. 微分ベクトルの大きさ

      G( x, y ) = sqrt( Yx( x, y )² + Yy( x, y )² )

      記号 sqrt は平方根をあらわす.

    なお,変数 gl_FragColor の r, g, b は色値として 0.0 〜 1.0 を有効な値とするため,求められた勾配値 G( x, y ) を係数によってスケーリングするなど,適宜,適切な明度で表示されるように工夫する.

プログラム例 (Visual Studio 2010 で作成)