テクスチャの作成


・・・ぶっちゃけ、テクスチャってけっこうめんどくさいです。

が、まぁ、使えると使えないとでは、見た目がかなり違ってくるし、
いちど使えるようになれば、
あとはオブジェクト指向の利を生かして
テクスチャ用のクラスとしてまとめ込んでしまえばたやすいモノ(に、なるはず)。
と、いうことで、ちょっとここらで頑張りましょうかね。

そうそう、
ここでの説明は、基本的にC#でOpenGLのテクスチャを作成する手順にのみ
焦点を当てていますので、
始めに、あらかじめこれに先立って、
他のサイトなどで資料やサンプルプログラムを見て
OpenGLのテクスチャ生成の手順を確認しておくことを、おすすめします。
(必ずしも、とはいいませんが。)


さて、
まずは、テクスチャ生成の手順をおおざっぱに確認。
こんな感じです。

  1. 画像ファイルをBitmapクラスを使って読み込む。
  2. 読み込んだBitmapから、画像データをbyte[]型の配列として取り出す。
  3. 画像のサイズをチェック、必要ならリサイズ。
  4. 以下、OpenGLに対する操作。
    テクスチャオブジェクトを作成。
  5. テクスチャのバインド。
  6. 画像データの割り当て。
  7. テクスチャパラメータの設定。
  8. (不要になったテクスチャの削除)

1. 画像ファイルをBitmapクラスを使って読み込む

OpenGLでは特定の形式の画像の読み込みはサポートされていないので、
bmp、jpg、pngなどの形式の画像をテクスチャなどとして使う場合には、
画像を読み込むための手段を、別途にDLLなどとして用意する必要があり、
結構面倒だったらしい(やったこと無いのでわかりませんが・・・)。

しかし、.net Frameworkでは、Bitmapクラスとして標準で
bmp、jpg、png、gif
これら4つの形式の画像ファイルを読み書きするための手段が用意されていているので、
C#ではとても簡単に扱うことができます。

つまり、C#+OpenGLでテクスチャを使おうとした時に、
これらの手間をぐっと省いて簡単に画像ファイルからテクスチャを作成できる、
ということになります。

では、
Bitmapクラスを使って、画像ファイルを読み込みます。

Bitmap bmp = new Bitmap( "texture.png" );

これだけ。
まぁ、この辺は特に問題ないはず。


2.画像データをbyte[]型の配列として取り出す

OpenGLでは、画像を扱うための特定の形式は用意されておらず、
1.で作成したbmpを直接渡すことはできません。
OpenGLでは、画像データはもっぱらbyte[]型の配列などとして扱うことになります。

そこで、bmpの画像データをbyte[]型に移し替えます。
BitmapクラスにはGetPixelという、画像の色を取得するメソッドが用意されていますが、
しかし、これを使うと処理が非常に遅くなってしまうので、
bmpがメモリ上に持っている画像データを直接コピーします。
そのためには、
Bitmap.LockBits
System.Runtime.InteropServices.Marshal.Copy
という、2つのメソッドを使うことになります。

ところが、です。
このBitmapクラス、画像のピクセルデータを
左上から右方向に順に並だ状態で持っているのですが、
OpenGLでは、画像は左下から右方向に順に並んでいるものとして扱うことになっています。
なので、 このまま作業を続けていくと最終的に
上下逆さまのテクスチャができあがってしまいます。

そこで、始めのうちにBitmapクラスに用意されている便利なメソッド
Bitmap.RotateFlip
を使ってさっさと上下を反転させておきます。

ま、あらかじめ上下ひっくり返った画像を用意しておくとか、
上下ひっくり返ったままにしておいて、
テクスチャを参照するときの座標のほうを上下ひっくり返すとか、
いくつか方法があるのですが、
今回は、一番わかりやすいこの方法ということで。

//画像の上下を反転。
bmp.RotateFlip( RotateFlipType.RotateNoneFlipY );

//bmpが持っている画像データをメモリ上に固定。
//読み取り専用・色要素は32ビットARGB(4色・各8ビット)。
BitmapData bmpData = bmp.LockBits(
                                     new Rectangle( 0, 0, bmp.Width, bmp.Height ),
                                     System.Drawing.Imaging.ImageLockMode.ReadOnly,
                                     System.Drawing.Imaging.PixelFormat.Format32bppArgb
                                  );

//画像データを格納するための配列。
//配列の長さは、(幅)*(高さ)*(色要素の数)。
byte[] imageByteArray = new byte[ bmp.Width * bmp.Height * 4 ];

//画像データのコピーをおこなう。
System.Runtime.InteropServices.Marshal.Copy(
                                               bmpData.Scan0,
                                               imageByteArray,
                                               0,
                                               imageByteArray.Length
                                           );

//メモリ上の固定を解除する。
bmp.UnlockBits( bmpData );

「画像データをメモリ上に固定」というのは、
C#ではガーベジコレクタなるモノが裏で動いているから必要になってくる作業らしいが、
詳しいことは省略。
よくわからない場合は、まぁ、とりあえずこういうもんだと思っておいて下さい。

・・・ここで、まためんどくさいのが、ちょっと、ありまして。
このままでは配列内の色要素の並び方が、「B, G, R, A, B, G, R, A ・・・」となっている
(Bitmapの画像データの並びがそうなってるので)のですが、
OpenGLでは「R, G, B, A, ・・・」という並び方が一般的なので、色要素の並び替えをしておきます。
つまり、各ピクセルのRとBを入れ替えれば良いわけです。

byte r, b;
int n;

for( int i = 0; i < bmp.Width * bmp.Height; ++i )
{
    n = i * 4;
    b = imageByteArray[n];
    r = imageByteArray[n + 2];

    imageByteArray[n] = r;
    imageByteArray[n + 2] = b;
}

これで、各ピクセルのRとBが入れ替わります。

ちなみに、bmp.GetPixelメソッドで1ピクセルずつ読み取って、配列の各要素に代入していく、
という方法も可能ですが、処理が非常に遅いので、おすすめしません。

*上のコードにもあるように、ここでは、画像を32ビットRGBA(4色・各8ビット)として扱います。
アルファ値はいらないからRGB(3色・各8ビット)でよい、という場合は、
bmp.LockBits( ... );
の、3つめの引数を「Format24bppRgb」に、
色要素の数は3つ、
後で出てくるGL_RGBAをGL_RGBに変えます。


3.画像サイズのチェックとリサイズ

やっかいなことに、OpnenGLでは、テクスチャとして用いる画像のサイズに条件があり、
「テクスチャのサイズは、nを整数として2nでなければならない。」
例えば、(幅*高さ) が、
(256* 128) とか、(512*1024) とか
でなければならないということ。

よって、テクスチャ用の画像は、
あらかじめこの条件を満たすようにリサイズしておくか、
プログラム上でOpenGLに渡す前にリサイズするか、
どちらかをしておかなければなりません。
ここでは、プログラム上でリサイズするテクニックを解説します。


・まずは、画像のサイズが2nであるかどうかをチェックします。
そのためには、ちびっとだけ数学を使います。
すなわち、
幅や高さについて、2を底として対数をとってやれば、それらが2の何乗であるかが解る、
という具合です。(たいしたことないですよね?)
つまり、こんな感じ。
float log_width  = Math.Log( bmp.Width, 2 );
float log_height = Math.Log( bmp.Height, 2 );


こうすると、
bmp.Width = 2log_width
bmp.Heighht = 2log_height
であるから、
log_widthとlog_heightが整数であれば、
サイズの条件を満たしているので、そのまま次のステップ4.へ進めます。
しかし、どちらか一方でも整数でなければ、
条件を満たしていないので、条件を満たすようにリサイズする必要があります。


・サイズの条件を満たしていない場合は、リサイズを行います。
画像のリサイズには、GLUに用意されている
gluScaleImage
という 関数を利用します。

2.で、画像データを 
imageByteArray
に格納したので、これに対してリサイズを行います。

//スケーリング後の画像のサイズ。
//条件に合うように拡大する。
int newWidth  = (int)Math.Pow( 2, (int)log_width + 1 );
int newHeight = (int)Math.Pow( 2, (int)log_height + 1 );

//スケーリング後の画像を格納する配列
byte[] scaledImage = new byte[newWidht * newHeight * 4];

//画像のスケーリング。
gluScaleImage( 
                 GL_RGBA,
                 width, height,
                 GL_UNSIGNED_BYTE,
                 imageByteArray,
                 newWidth, newHeight,
                 GL_UNSIGNED_BYTE,
                 scaledImage
             );

//スケーリングされた画像と入れ替える。
imageByteArray = scaledImage;


これで、画像がサイズの条件を満たすようにリサイズされるはずです。
 

4.テクスチャオブジェクトの作成
5.テクスチャのバインド
6.画像データの割り当て
7.テクスチャパラメータの設定

4.~7.は、まとめてやってしまいます。
ここからは、OpenGLに対する操作がメインです。
このあたりでやることは他の言語でやってもほとんど同じなので、
詳細等は、詳しく説明しているサイトがよそにあるので、そちらで確認されたし。

//OpenGLから割り当てられる識別番号を保持しておく変数。
uint[] name = new uint[1];

//テクスチャオブジェクトの作成。
glGenTextures( 1, name );

//テクスチャのバインド。
glBindTexture( GL_TEXTURE_2D, name[0] );

//メモリ上の画像データの並び方を指定。
glPixelStorei( GL_UNPACK_ALIGNMENT, 1 );

//テクスチャへ画像データを割り当てる。
glTexImage2D(
                GL_TEXTURE_2D,
                0,
                4,
                width, height,
                0,
                GL_RGBA,
                GL_UNSIGNED_BYTE,
                imageByteArray
            );

//テクスチャパラメータの設定をお忘れ無く。
//最低限これをしておかないと、テクスチャが表示されない場合アリ。
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );


これでやっと、テクスチャが作成できました。
作成したテクスチャを使用する場合は、
glBindTexture( glTEXTURE_2D, name[0] );
として、テクスチャをバインド(使用可能な状態にする)してから、
操作を行います。

・・・ふぅ。
やっぱり、こうやって解説してみると、はっきりいって、しんどいです。
で、いちいちこんな面倒なことを、あれやこれやと考えずに済むように、
テクスチャのためのクラスを作っておくべきだ、
と、冒頭で言ってたわけです。


8.不要になったテクスチャの削除

テクスチャが不要になった場合は、
きちんとテクスチャオブジェクトの解放の処理を行うべきでしょう。
まぁ、これは簡単です。

glDeleteTextures( 1, name );




その他・諸注意など

「C# OpenGL Framework」をOpneGLラッパーとして使っている場合の注意。
「Basic Edition」の「opengl.cs」について、いくつか注意すべきところが。

まず、
glu.ScaleImage
メソッドは、そのままでは呼び出し時にエラーが発生する。
opengl.csのソースコードを見てみると、メソッドの属性が、
[DllImport(GLUDLLName, EntryPoint = "ScaleImage")]
になってますが、
[DllImport(GLUDLLName, EntryPoint = "gluScaleImage")]
の間違いです。"glu"を書き加えて下さい。
(なぜかここだけ間違ってる。)

次。
いくつか、そのままでは使いにくい引数のメソッドがあるので、
使い勝手がよいように、オーバーロードメソッドを追加します。

追加オーバーロード・その1.
ScaleImageを使用する際に、引数がIntPtrでは面倒なので、byte[]に。

[DllImport( GLUDLLName, EntryPoint = "gluScaleImage" )]
public static extern void ScaleImage( int format, int widthin, int heightin, inttypein, byte[] datain, int widthout, int heightout,int typeout, byte[] dataout );


追加オーバーロード・その2.
GenTextureとBindTextureのテクスチャの識別名を指定する引数は、下のようにuintなのに、
BindTexture(int target, uint texture);
GenTextures(int n, uint[] textures);
なぜかDeleteTexturesはintなので、uint[]のオバーロードを追加。
[DllImport( DLLName, EntryPoint = "glDeleteTextures" )]
public static extern void DeleteTextures( int n, uint[]textures );
    



Sample Program

さて、テクスチャを使って簡単な描画を行うサンプルプログラムを用意しておきました。

内容としては、
テクスチャを貼り付けた1枚の四角いポリゴンが、
タイマイベントを利用したアニメーションで、ゆっくりと回転する、
という、単純なものです。

Texture2D
というクラスを作ってみました。
テクスチャを扱うためのクラスです。
コンストラクタで画像ファイル(bmp、jpg、png、gif)のパスを指定するだけで、
全自動でRGBAフォーマットのテクスチャを作成してくれます。
また、このクラスはIDisposableインターフェイスを実装しており、
Disposeメソッド内で、テクスチャオブジェクトの解放を行うようになってます。

はっきり言って、このTexture2Dクラスを他のOpenGLプログラムに放り込んでしまえば、
上の長ったらしい解説を理解してなくても、テクスチャがサクッと使えるようになってしまいます。

・・・が、
このクラスは最低限のことしか実装していないので、
エラーに対処したり、改造したりするには、
きちんとテクスチャうんぬんについて理解しておく必要があります。

サンプルプログラムのダウンロード:
OpenGLTextureSample.zip


<back to OpenGL menu>