OpenGLプログラミング/モダンOpenGL チュートリアル 06

出典: フリー教科書『ウィキブックス(Wikibooks)』
ナビゲーションに移動 検索に移動

テクスチャのロード[編集]

Our texture, in 2D

テクスチャをロードするには、JPEGやPNGのような、特定の形式で画像をロードするコードが必要です。通常、最終的なプログラムは、様々な画像フォーマットをサポートする、SDL_image、SFML、Irrlicht、などのような一般的なライブラリを使用するので、画像読み込みコードを自前で記述する必要はありません。 SOIL(下記参照)などの特化したライブラリも興味深いかもしれません。

最初のステップでは、基本を理解するために低レベルで画像を操作する必要があるので、トリックを使っていきます:GIMPは、画像をC言語のソースコードとしてエクスポートすることができ、私たちのプログラムからそのまま読むことができます! 右のGIMPのスクリーンショットのsaveオプションを使用しました。

Exporting image as C from GIMP

需要があれば、PNMのようなシンプルな形式、またはBMPやTGAのサブセットを読み込むための、特別なチュートリアルを提供するかもしれません(これら2つもシンプルですが、圧縮や様々な形式をサポートしているので、すべてのオプションをサポートするのは難しくなります)。

注:Cコードとして画像をバンドルすることはメモリ効率がひどく悪いので、それを一般化しないでください。 技術的には:プログラムのBSSセグメントではなくヒープに保存されるので、解放することができません。

注2:コードリポジトリでres_texture.xcfとしてGIMPのソースを見つけることができます。

res_texture.cを修正したときに自動的にアプリケーションを再構築するには、Makefileにこれを追加します:

cube.o: res_texture.c

テクスチャOpenGLバッファを作成する[編集]

バッファは、基本的にはグラフィックカード内部のメモリスロットなので、OpenGLは非常にすばやくそこにアクセスすることができます。

/* Globals */
GLuint texture_id;
GLint uniform_mytexture;
/* init_resources */
  glGenTextures(1, &texture_id);
  glBindTexture(GL_TEXTURE_2D, texture_id);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexImage2D(GL_TEXTURE_2D, // target
	       0,  // level, 0 = base, no minimap,
	       GL_RGB, // internalformat
	       res_texture.width,  // width
	       res_texture.height,  // height
	       0,  // border, always 0 in OpenGL ES
	       GL_RGB,  // format
	       GL_UNSIGNED_BYTE, // type
	       res_texture.pixel_data);
/* render */
  glActiveTexture(GL_TEXTURE0);
  glBindTexture(GL_TEXTURE_2D, texture_id);
  glUniform1i(uniform_mytexture, /*GL_TEXTURE*/0);
/* free_resources */
  glDeleteTextures(1, &texture_id);

テクスチャ座標[編集]

さて、各頂点をテクスチャのどこに配置するかを指示する必要があります。 このために、 v_color属性を置き換えて、 texcoordを使用した頂点シェーダにします:

GLint attribute_coord3d, attribute_v_color, attribute_texcoord;
/* init_resources */
  attribute_name = "texcoord";
  attribute_texcoord = glGetAttribLocation(program, attribute_name);
  if (attribute_texcoord == -1) {
    fprintf(stderr, "Could not bind attribute %s\n", attribute_name);
    return 0;
  }

今、マッピングしていくのはテクスチャのどの部分に、例えば前面の左上隅になるのでしょうか? まあ、それは場合によります:

  • 前面の場合:テクスチャの左上隅
  • 上面の場合:テクスチャの左下隅

複数のテクスチャポイントが同じ頂点にくっついていることがわかります。 頂点シェーダはどちらを選べばいいかを判断できません。

なので面あたり4つの頂点でキューブを書き換える必要があり、頂点の再使用はしません。

スタートのために、前面だけを扱っていきます。 簡単です! 私たちは2つの最初の三角形(6つの最初の頂点)だけを表示する必要があります:

  glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0);

したがって、私たちのテクスチャ座標は[0、1]の範囲内にあり、x軸は左から右に、そしてy軸は下から上になります。

  /* init_resources */
  GLfloat cube_texcoords[] = {
    // front
    0.0, 0.0,
    1.0, 0.0,
    1.0, 1.0,
    0.0, 1.0,
  };
  glGenBuffers(1, &vbo_cube_texcoords);
  glBindBuffer(GL_ARRAY_BUFFER, vbo_cube_texcoords);
  glBufferData(GL_ARRAY_BUFFER, sizeof(cube_texcoords), cube_texcoords, GL_STATIC_DRAW);
  /* onDisplay */
  glEnableVertexAttribArray(attribute_texcoord);
  glBindBuffer(GL_ARRAY_BUFFER, vbo_cube_texcoords);
  glVertexAttribPointer(
    attribute_texcoord, // attribute
    2,                  // number of elements per vertex, here (x,y)
    GL_FLOAT,           // the type of each element
    GL_FALSE,           // take our values as-is
    0,                  // no extra data between each position
    0                   // offset of first element
  );

頂点シェーダ:

attribute vec3 coord3d;
attribute vec2 texcoord;
varying vec2 f_texcoord;
uniform mat4 mvp;

void main(void) {
  gl_Position = mvp * vec4(coord3d, 1.0);
  f_texcoord = texcoord;
}

フラグメントシェーダ:

varying vec2 f_texcoord;
uniform sampler2D mytexture;

void main(void) {
  gl_FragColor = texture2D(mytexture, f_texcoord);
}
Something is wrong...

おっと何が起こっているのでしょうか? テクスチャが逆さまです!

OpenGLの規約(左下隅が原点)は、2Dアプリケーション(左上隅が原点)とは異なります。 これの修正は、次のいずれかで実行できます:

  • ピクセルラインを下から上へ読み込む
  • ピクセルラインを入れ替える
  • テクスチャのY座標を入れ替える

ほとんどのグラフィック·ライブラリは、2D規約におけるピクセルの配列を返します。 しかし、 DevIL は、原点に配置するオプションを持ち、この問題を回避できます。 あるいは、BMPやTGAのようないくつかの形式は、ネイティブで下から上にピクセルラインを格納し(これがTGA形式の重さにもかかわらず3Dの開発者の間で一定の人気がある理由といえるかもしれません)、カスタムローダーを書く場合には便利です。

ピクセルラインの入れ替えも、実行時にCコードで行えます。 Pythonのようなハイレベルな言語でプログラムするのであれば、1行で行うことさえできます。 欠点は、この余分なステップが原因で、テクスチャのロードが幾分遅くなるということです。

テクスチャ座標を逆にすることは、私たちにとって最も簡単な方法で、フラグメントシェーダでそれを行えます:

void main(void) {
  vec2 flipped_texcoord = vec2(f_texcoord.x, 1.0 - f_texcoord.y);
  gl_FragColor = texture2D(mytexture, flipped_texcoord);
}

OK、技術的には最初は他の方向のテクスチャ座標で書くこともできました - しかし他の3Dアプリケーションは私たちが記述したような動作をする傾向があります。

打ちつけて完全なキューブに[編集]

説明してきたように、それぞれの面ごとに独立した頂点を指定します:

  GLfloat cube_vertices[] = {
    // front
    -1.0, -1.0,  1.0,
     1.0, -1.0,  1.0,
     1.0,  1.0,  1.0,
    -1.0,  1.0,  1.0,
    // top
    -1.0,  1.0,  1.0,
     1.0,  1.0,  1.0,
     1.0,  1.0, -1.0,
    -1.0,  1.0, -1.0,
    // back
     1.0, -1.0, -1.0,
    -1.0, -1.0, -1.0,
    -1.0,  1.0, -1.0,
     1.0,  1.0, -1.0,
    // bottom
    -1.0, -1.0, -1.0,
     1.0, -1.0, -1.0,
     1.0, -1.0,  1.0,
    -1.0, -1.0,  1.0,
    // left
    -1.0, -1.0, -1.0,
    -1.0, -1.0,  1.0,
    -1.0,  1.0,  1.0,
    -1.0,  1.0, -1.0,
    // right
     1.0, -1.0,  1.0,
     1.0, -1.0, -1.0,
     1.0,  1.0, -1.0,
     1.0,  1.0,  1.0,
  };

F各面について、頂点は反時計回りに追加されます(見る人がその面を向いているとき)。 その結果として、テクスチャマッピングは、すべての面で同一になります。

  GLfloat cube_texcoords[2*4*6] = {
    // front
    0.0, 0.0,
    1.0, 0.0,
    1.0, 1.0,
    0.0, 1.0,
  };
  for (int i = 1; i < 6; i++)
    memcpy(&cube_texcoords[i*4*2], &cube_texcoords[0], 2*4*sizeof(GLfloat));

ここでは、前面についてマッピングを指定し、それを残りの5面すべてにコピーしました。

I面が反時計回りではなく時計回りの場合、テクスチャはミラーされて表示されます。 方向には規約は無く、テクスチャ座標が正しく頂点にマップされていることを確認する必要があります。

キューブ要素も同様に、インデックス(x, x+1, x+2), (x+2, x+3, x) を持つ2つの三角形で書かれます:

  GLushort cube_elements[] = {
    // front
     0,  1,  2,
     2,  3,  0,
    // top
     4,  5,  6,
     6,  7,  4,
    // back
     8,  9, 10,
    10, 11,  8,
    // bottom
    12, 13, 14,
    14, 15, 12,
    // left
    16, 17, 18,
    18, 19, 16,
    // right
    20, 21, 22,
    22, 23, 20,
  };
  ...
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo_cube_elements);
  int size;  glGetBufferParameteriv(GL_ELEMENT_ARRAY_BUFFER, GL_BUFFER_SIZE, &size);
  glDrawElements(GL_TRIANGLES, size/sizeof(GLushort), GL_UNSIGNED_SHORT, 0);


Fly, cube, fly!

付加的な楽しみのために、そして底面をチェックするために、 NeHe's flying cube tutorialで説明している3回転の動きを onIdleで実装してみましょう:

  float angle = glutGet(GLUT_ELAPSED_TIME) / 1000.0 * 15;  // base 15° per second
  glm::mat4 anim = \
    glm::rotate(glm::mat4(1.0f), angle*3.0f, glm::vec3(1, 0, 0)) *  // X axis
    glm::rotate(glm::mat4(1.0f), angle*2.0f, glm::vec3(0, 1, 0)) *  // Y axis
    glm::rotate(glm::mat4(1.0f), angle*4.0f, glm::vec3(0, 0, 1));   // Z axis

できあがりです!

Using SOIL[編集]

WIP

SOIL provides a way to load an image file in PNG, JPG and a few other formats, designed for OpenGL integration. It's a pretty minimal library with no dependency. It's used under the hood by SFML (although SFML also uses libjpeg and libpng directly).

Install it (look for a package named libsoil, libsoil-dev, or something similar).

Reference it in your Makefile:

LDLIBS=-lglut -lSOIL -lGLEW -lGL -lm

One high-level function allows you to upload it directly to the OpenGL context:

  glActiveTexture(GL_TEXTURE0);
  GLuint texture_id = SOIL_load_OGL_texture
    (
     "res_texture.png",
     SOIL_LOAD_AUTO,
     SOIL_CREATE_NEW_ID,
     SOIL_FLAG_INVERT_Y
     );
  if(texture_id == 0)
    cerr << "SOIL loading error: '" << SOIL_last_result() << "' (" << "res_texture.png" << ")" << endl;
  • SOIL_FLAG_INVERT_Y deals with the reverse-Y-coordinates issue we experienced above.
  • SOIL also adapt NPOT (non power of 2) textures, when the graphic card doesn't handle these directly

Note that with this method, you do not have access to the image dimensions. Do get them, you need to use a lower-level API:

  int width, height;
  unsigned char* img = SOIL_load_image("res_texture.png", &width, &height, NULL, 0);
  glGenTextures(...
  ...

Further reading[編集]

テンプレート:OpenGL Programming BottomNav