OpenGL ES2(5) テクスチャマッピング (2012/10/14, 2017/12/24)

前回は三角形をX軸を中心に回転させるサンプルを作成しましたが、今回はその三角形にテクスチャを貼ってみます。 「テクスチャ(texture)」とは織物の織り方や手触りなどの質感を表す単語で、コンピュータグラフィックスでは物体の表面に画像を貼り付けることをテクスチャマッピングと呼びます。 比較的少ない頂点で構成された物体でも「それらしい」画面にすることができるため、ゲームなどの3DCGには必ずといっていいほど使われています。

テクスチャマッピングした物体を表示するには、画像(テクスチャ)の位置と多角形 (三角形) の頂点を対応(マッピング)づけたデータを用意します。 頂点座標は3次元で (x, y, z) のように表しますが、テクスチャ座標は2次元のため(s, t) のように表します。 s軸が横、t軸が縦になります。貼り付ける三角形とテクスチャの向きや形は一致している必要はなく、テクスチャは貼り付ける表面に対応して適当に伸び縮みします。 多くの三角形に対して、一枚のテクスチャの色々な部分が貼り付けられることが一般的です。

上の図では今回のサンプルを実行しているスクリーンショットと使用した画像ファイルの関係を示しています。三角形の3つの頂点に対応する画像ファイルの位置を示すテクスチャ座標 (図中のsとt) を追加の情報として渡します。三角形が回転して裏になった時にはテクスチャも裏から見た形になります。

画像ファイルの読み込み

OpenGL には画像を読み込む関数がないので自作するかライブラリを利用することになります。ここでは構造が簡単なWindowsのbmp形式の画像ファイルを読み込んでテクスチャとして利用する関数を作成しました。

ファイルの読み込み

まず最初に指定されたファイル名のファイルをすべて引数で指定したバッファに読み込みます。バッファは別に用意してバッファのアドレスを関数に渡します。 ファイルサイズは MAXSIZE で指定した大きさ以上のファイルは読み込みません。

int LoadFile(char *filename, void *buffer)
{
  FILE *fp;
  long fsize;
  int n_read = 0;

  fp = fopen(filename, "rb");
  if (fp > 0) {
    // 先にファイルサイズを確認
    fseek(fp, 0L, SEEK_END);
    fsize = ftell(fp);
    if (fsize >= MAXSIZE) {
      fclose(fp);
      return -1;
    }
    // ファイルを先頭からすべて読み込む
    fseek(fp, 0L, SEEK_SET);
    n_read = fread((void *)buffer, 1, fsize, fp);
    fclose(fp);
    return n_read;
  }
  return -1;
}

bmpフォーマットのチェック

読み込んだファイルが有効な bmp フォーマットであるかどうかチェックして、画像データの先頭位置を取得します。 Windows の bmp フォーマットには複数の形式がありますが、最も古くて簡単な形式のみ (V4、V5形式はサポートしない) とします。Windowsのペイントブラシ、LinuxのGimp、MacOSX のプレビューで作成できる形式はすべて扱えるはずです。

画像情報の記録

読み込んだ bmp 画像の情報とテクスチャ配列を格納するために構造体を TexureType 型として定義します。

typedef struct {
    int  fsize;
    unsigned char *pdata;    // 画像ファイルのピクセルデータ
    unsigned char *TexData;  // テクスチャのピクセルデータ
    BITMAPFILEHEADER *bmpheader;
    BITMAPINFOHEADER *bmpinfo;
    int  BmpSize;
    int  BmpOffBits;
    int  BmpWidth;           // 画像の幅
    int  BmpHeight;          // 画像の高さ(負ならば反転)
    int  BmpBit;             // 画像のビット深度
    int  BmpLine;
    int  initial_alpha;
    GLuint  texname;
} TexureType;
bmpフォーマットのチェックと記録

bmpCheck 関数は bmp フォーマットのチェックをして問題なければ、画像の幅、高さ、ビット深度などの情報を引数として渡された TexureType型の変数に記録します。

int bmpCheck(TexureType *tt, void* buffer)
{
  tt->bmpheader = (BITMAPFILEHEADER *)buffer;
  // bmp フォーマットのシグネチャのチェック
  if (tt->bmpheader->bfType != ('B' | ('M' << 8))) {
    return 0;
  }
  tt->BmpSize = tt->bmpheader->bfSize;
  tt->BmpOffBits = tt->bmpheader->bfOffBits;
  tt->bmpinfo = (BITMAPINFOHEADER *)(buffer + sizeof(BITMAPFILEHEADER));
  // bmp フォーマットの形式チェック
  if (tt->bmpinfo->biSize == 40) {
    tt->BmpWidth = tt->bmpinfo->biWidth;
    tt->BmpHeight = tt->bmpinfo->biHeight;
    tt->BmpBit = tt->bmpinfo->biBitCount;
    tt->BmpLine = (tt->BmpWidth * tt->BmpBit) / 8;
    tt->BmpLine = (tt->BmpLine + 3) / 4 * 4;
    // 画像ファイルのピクセルデータの先頭アドレス
    tt->pdata = buffer + tt->bmpheader->bfOffBits;
    return 1;
  } else {
    return 0;
  }
}

テクスチャ配列の作成

読み込んだ画像ファイルからテクスチャとして利用できる形式に変換する必要があります。24ビット、16ビット、モノクロの bmp 画像からテクスチャ用の配列に変換します。テクスチャ配列は赤青緑各色1バイトと透明度1バイトで順次メモリ上に格納します。bmp 画像の色数 (24ビット、16ビット、モノクロ) に合わせてピクセルデータをテクスチャ用の配列に各色1バイトの値に変換して格納します。

bmp フォーマットでは画像の高さデータ (BmpHeight) が負の場合は上の図と異なって左上が原点となり、上から順番にピクセルデータが格納されているため、高さデータ (BmpHeight) が負の場合は bmp 画像のピクセルデータは上から取得しています。 また、各ピクセルはbmpファイルでは青、緑、赤の順に格納されていますが、テクスチャ用に赤、緑、青の順に変換しています。

void makeTexture(TexureType *tt, int alpha)
{
  int color, x, y;
  int dir = 0;

  // 引数で指定された透明度を設定
  tt->initial_alpha = alpha;
  // 高さ方向が反転(左上が原点)している場合
  if (tt->BmpHeight < 0) {
    tt->BmpHeight = -tt->BmpHeight;
    dir = -1;
  }

  // テクスチャ用にメモリを確保
  tt->TexData = malloc(sizeof(char)*tt->BmpWidth * tt->BmpHeight * 4);
  // ピクセルデータ先頭からテクスチャ用に変換
  for (y=0; y < tt->BmpHeight; y++) {
    for (x=0; x < tt->BmpWidth; x++) {
      int bitdata;
      int offset, n;

      if (dir < 0) {
        // 縦が反転(左上が原点)している場合
        offset = (tt->BmpHeight - 1 - y)*tt->BmpLine + x*tt->BmpBit/8;
      } else {
        // 左下が原点の場合
        offset = (y * tt->BmpLine) + x * (tt->BmpBit / 8);
      }
      n = (y*tt->BmpHeight+x)*4;
      switch(tt->BmpBit) {
        case 32 :
        case 24 : // 32ビットビットマップ
          tt->TexData[n+2] = tt->pdata[offset];    // B
          tt->TexData[n+1] = tt->pdata[offset+1];  // G
          tt->TexData[n+0] = tt->pdata[offset+2];  // R
          tt->TexData[n+3] = alpha;  // A
          break;
        case 16 : // 16ビットビットマップ
          color = tt->pdata[offset]|(tt->pdata[offset+1]<<8);
          tt->TexData[n+2] = (color & 0x1F) << 3;  // B
          tt->TexData[n+1] = (color & 0x3E0) >> 2; // G
          tt->TexData[n+0] = (color & 0x7C00) >> 7;// R
          tt->TexData[n+3] = alpha;  // A
          break;
        case 1 : // モノクロビットマップ
          bitdata = tt->pdata[offset] & (0x80 >> (x % 8));
          color = bitdata?255:0;
          tt->TexData[n+2] = color;  // B
          tt->TexData[n+1] = color;  // G
          tt->TexData[n+0] = color;  // R
          tt->TexData[n+3] = alpha;  // A
          break;
      }
    }
  }
}

テクスチャの作成

OpenGL ES2 にテクスチャを登録して、ラッピングモード、拡大縮小の方法などの設定を行います。頂点用のバッファーオブジェクトと同様にテクスチャオブジェクトを生成して、そのテクスチャオブジェクトを示す番号 (下のコードでは tt->texname) を取得した後、その番号を使って、使用するテクスチャを指定します。そのテクスチャに対して各種設定を行います。 glTexImage2D で画像データをテクスチャに割り当てます。

int CreateTexture(TexureType *tt)
{
  // テクスチャオブジェクトを生成
  glGenTextures(1, &tt->texname);
  // 使用するテクスチャを指定
  glBindTexture(GL_TEXTURE_2D, tt->texname);
  // 画像データのメモリ上の構造を指定
  glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
  // 横方向にテクスチャを繰り返す
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
  // 横方向にテクスチャを繰り返す
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
  // 拡大する場合は直線補間する
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  // 縮小する場合は直線補間する
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  // テクスチャ用配列(画像データ)をテクスチャに割り当て
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, tt->BmpWidth, tt->BmpHeight, 0,
                GL_RGBA, GL_UNSIGNED_BYTE, tt->TexData);
  return 1;
}

物体の定義

これまでと同じく 今回のサンプル でも三角形1枚を使いますが、前々回の頂点情報 ではテクスチャ座標の呼び方がDirectX 形式(uv座標)になっていたため、OpenGLで使われるs座標、t座標に修正しました。

typedef struct {
    GLfloat x, y, z;    // 頂点座標
    GLfloat s, t;       // テクスチャ座標
} VertexType;

また、三角形の頂点のテクスチャ座標 (s, t) を左下が原点のOpenGLのテクスチャ座標に合わせて変更しています。

VertexType vObj[] = {
  {.x = -0.5f, .y = -0.5f, .z = 0.0f, .s = 0.0f, .t = 0.0f},
  {.x =  0.5f, .y = -0.5f, .z = 0.0f, .s = 1.0f, .t = 0.0f},
  {.x =  0.0f, .y =  0.5f, .z = 0.0f, .s = 0.5f, .t = 1.0f},
};

フラグメントシェーダ

バーテックスシェーダは前回のシェーダと同じですが、フラグメントシェーダだけテクスチャを受け取るため変更します。texture2D 関数は引数で指定した位置のテクスチャの色を返します。テクスチャ中の位置 vTex はバーテックスシェーダとフラグメントシェーダの両方で「varying」と修飾された変数なので、バーテックスシェーダからフラグメントシェーダに渡るときにフラグメントシェーダが処理するピクセル位置に対応した値に自動的に補間されます。

precision mediump float;
varying   vec2  vTex;
uniform sampler2D uTexture;
void main()
{
  gl_FragColor = texture2D(uTexture,vec2(vTex.s,vTex.t));
}

描画

前々回の Draw関数 との違いを赤字で示しています。テクスチャを有効にして、使うテクスチャをバインドすることを指定するだけです。 実際の描画はフラグメントシェーダで行われるため、テクスチャに関する設定は2行だけです。

void Draw ()
{
  glUseProgram(g_program);

  glActiveTexture(GL_TEXTURE0);
  glBindTexture(GL_TEXTURE_2D, g_tt.texname);

  glBindBuffer(GL_ARRAY_BUFFER, g_vbo);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, g_ibo);
  glEnableVertexAttribArray(g_sp.aPosition);
  glEnableVertexAttribArray(g_sp.aTex);
  glVertexAttribPointer(g_sp.aPosition, 3, GL_FLOAT, GL_FALSE, 20, (void*)0);
  glVertexAttribPointer(g_sp.aTex, 2, GL_FLOAT, GL_FALSE, 20, (void*)12);
  glEnableVertexAttribArray(0);
  glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_SHORT, 0);
}

main

以下のコードは main 関数全体です。前回からの違いは赤で示しています。

int main ( int argc, char *argv[] )
{
  unsigned int frames = 0;
  int   res;
  Mat4  viewMat;
  Mat4  rotMat;
  Mat4  modelMat;
  float aspect;
  int   size;

  bcm_host_init();
  res = WinCreate(&g_sc);
  if (!res) return 0;
  res = SurfaceCreate(&g_sc);
  if (!res) return 0;
  res = InitShaders(&g_program, vShaderSrc, fShaderSrc);
  if (!res) return 0;

  createBuffer();

  // 画像ファイルの読み込み
  size = LoadFile("num256.bmp", (void *)g_bmpbuffer);
  printf("LoadFile %d \n", size);
  // bmp画像情報の取得
  bmpCheck(&g_tt, (void *)&g_bmpbuffer);
  // テクスチャ配列の作成
  makeTexture(&g_tt, 255);
  // テクスチャの作成
  createTexture(&g_tt);

  glUseProgram(g_program);
  g_sp.aPosition = glGetAttribLocation(g_program, "aPosition");
  g_sp.aTex = glGetAttribLocation(g_program, "aTex"");
  g_sp.uVpMatrix = glGetUniformLocation(g_program, "uVpMatrix");
  g_sp.uModelMatrix = glGetUniformLocation(g_program, "uModelMatrix");

  aspect = (float)g_sc.width / (float)g_sc.height;
  makeProjectionMatrix(&g_sp.VpMatrix, 1, 1000, 53, aspect);
  makeUnit(&viewMat);
  setPosition(&viewMat, 0, 0, -2);
  mulMatrix(&g_sp.VpMatrix, &g_sp.VpMatrix, &viewMat);
  glUniformMatrix4fv(g_sp.uVpMatrix, 1, GL_FALSE, g_sp.VpMatrix.m);

  makeUnit(&modelMat);
  glUniformMatrix4fv(g_sp.uModelMatrix, 1, GL_FALSE, modelMat.m);

  makeUnit(&rotMat);
  setRotationX(&rotMat, 0.5); // 30 度/秒

  glEnable(GL_DEPTH_TEST);
  glClearColor(0.4f, 0.2f, 0.0f, 1.0f);

  // 20秒間(1200frame / 60fps) 繰り返し
  while(frames < 1200) {
    glViewport(0, 0, g_sc.width, g_sc.height);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // X軸中心に回転
    mulMatrix(&modelMat, &modelMat, &rotMat);
    glUniformMatrix4fv(g_sp.uModelMatrix, 1, GL_FALSE, modelMat.m);

    Draw();
    eglSwapBuffers(g_sc.display, g_sc.surface);
    frames++;
  }
  return 0;
}

ソースのダウンロードと使い方

テクスチャが貼られた三角形が X軸まわりに回転するだけのサンプルのソースです。

triangle.c が今回のソース全体、triangle はコンパイルした実行ファイル、build はコンパイル用のシェルスクリプトです。

jun@raspberrypi ~/triangle03a $ ls -lt
-rwxr-xr-x 1 pi pi    211 Dec  7 16:10 build
-rwxr--r-- 1 pi pi 131126 Oct 30  2008 num256.bmp
-rwxr-xr-x 1 pi pi  19760 Dec  7 16:14 triangle
-rw-r--r-- 1 pi pi  14958 Dec  7 16:14 triangle.c

公式OSの Raspbian の場合、pi ユーザ(または video グループ権限を持つユーザ) で以下のようにファイルを解凍して、コンパイル、実行できます。20秒間動作して終了します。

$ tar zxf triangle03a.tar.gz
$ cd triangle03a
$ ./build
$ ./triangle

テクスチャ以外は前回のサンプルと同じですが、テクスチャによって上下と表裏が見分けやすくなり回転の方向が良く分かるようになりました。


続く...