プログラミング 見習い

プログラミングの勉強の学習日記的なブログ

OpenGLを学ぼうと足掻く 三角形の表示

あぁ何もわかっていないなと。
何も知らないし、何も出来ないし。。。
無知無能な人間なんだから選り好みせずに勉強しようと最近強く思うようになってきました。

ということで今日からOpenGLについて勉強してみようと思います。

自分の使っているオンボロ中古PCのGPUが対応しているOpenGLのバージョンは2.1でした。
多分これって結構古いのでプログラムも古い感じになってしまうと思います。。。

はっきり言って全くの初心者みたいなものなので、まず三角形を描画するプログラムを作ってみて全体の感じを掴んでみることにします。

とりあえず初心者はGLUTっていうライブラリを使っとけば間違いないみたいなのでそうすることにします。
他にも拡張機能にアクセスするためにGLEWというライブラリを入れたりしないといけないみたいなのでそうします。(これらはSynapticで調べればすぐに出てきましたので、それをインストールしました。)

それが終われば、もうOpenGLを使ったプログラムが作成できるみたいです。

main関数は以下のようになりました。

int main(int argc, char *argv[])
{
  glutInit(&argc, argv);
  glutInitWindowSize(500, 500);
  glutInitWindowPosition(10, 10);
  glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH);
  glutCreateWindow("OpenGL test01");

  InitializeGL();

  glutDisplayFunc(DisplayGL);
  glutMainLoop();
}

次に、初期化関数InitializeGLと、glutDisplayFuncに登録されているDisplayGL関数を実装します。

まず、InitializeGLを実装していきます。

void InitializeGL()
{
  std::cout << "GLEWの初期化" << std::endl;
  GLenum glewStatus = glewInit();
  if (glewStatus != GLEW_OK)
  {
    std::cerr << "Error: " << glewGetErrorString(glewStatus) << std::endl;
    std::exit(1);
  }

GLEWを使用するには、まずこんな感じに初期化しないといけないみたいです。

次に頂点シェーダを作成、コンパイルしていきます。
頂点シェーダの作成にはglCreateShaderを使い、引数にGL_VERTEX_SHADERを渡します。
また、この関数は頂点シェーダオブジェクトの参照に使う値(ハンドルというらしい)を返すのでそれを拾わないといけません。
もしエラーが生じれば0が返されるので、それを使ってエラー処理も行います。

  GLuint vertShader = glCreateShader(GL_VERTEX_SHADER);
  if (0 == vertShader)
  {
    std::cerr << "頂点シェーダ作成エラー" << std::endl;
    std::exit(1);
  }

次に頂点シェーダのソースコードを先ほど作成したシェーダオブジェクトにコピーします。
シェーダのソースコードはもちろん他のプログラミング言語同様、単なる文字列です。
それをシェーダオブジェクトにコピーするにはglShaderSourceを使うらしいです。
この関数は4つの引数をとります。
第一引数はシェーダオブジェクトのハンドル、第二引数はシェーダのソースコードの数、第三引数はソースコード文字列の配列へのポインタ、最後の引数は各ソースコード文字列の長さを持つGLint型の値の配列を渡すみたいですが、とりあえずNULLにしとけばいいみたいです。この場合各ソースコード文字列がNULL文字で終わることを意味するらしいです。

  const GLchar* vertShaderCode =
      "#version 120\n"
      "attribute vec3 VertexPosition;"
      "attribute vec3 VertexColor;"
      "varying vec3 Color;"
      "void main() {"
      "Color = VertexColor;"
      "gl_Position = vec4(VertexPosition, 1.0);"
      "}";
  glShaderSource(vertShader, 1, &vertShaderCode, NULL);

今はシェーダのソースコードは1つしか無いので、第二引数には1、第三引数には文字列のポインタを渡しています。

シェーダのソースコードに関しては、そのままなので特に書くことはないですね。
ただ、現在のOpenGL(GLSL)ではattribute修飾子やvarying修飾子を使わずに、in修飾子とout修飾子を使うらしいです。
僕のパソコンのOpenGLはバージョン2.1(GLSL1.2)らしいので、それに合わせてこれらの古い修飾子を使っています。

最後にこの読み込んだソースコードコンパイルします。
それにはglCompileShaderを使い、引数にシェーダオブジェクトのハンドルを渡します。

  glCompileShader(vertShader);

  GLint result;
  glGetShaderiv(vertShader, GL_COMPILE_STATUS, &result);
  if (GL_FALSE == result)
  {
    std::cerr << "頂点シェーダのコンパイルに失敗しました" << std::endl;
    std::exit(1);
  }

エラー処理は、、、よくわからないけどこんな感じでやればいいみたいです。

次にフラグメントシェーダの作成とコンパイルを行いますが、これはほとんど頂点シェーダの作成とコンパイルの手順と同じです。

  GLuint fragShader = glCreateShader(GL_FRAGMENT_SHADER);
  if (0 == fragShader)
  {
    std::cerr << "フラグメントシェーダ作成エラー" << std::endl;
    std::exit(1);
  }
  const GLchar* fragShaderCode =
      "#version 120\n"
      "varying vec3 Color;"
      "void main() {"
      "gl_FragColor = vec4(Color, 1.0);"
      "}";
  glShaderSource(fragShader, 1, &fragShaderCode, NULL);
  glCompileShader(fragShader);

  glGetShaderiv(fragShader, GL_COMPILE_STATUS, &result);
  if (GL_FALSE == result)
  {
    std::cerr << "フラグメントシェーダのコンパイルに失敗しました" << std::endl;
    std::exit(1);
  }

頂点シェーダで頂点ごとに設定した色情報は正しい遠近法で補間されて、フラグメントシェーダに渡されます。
これを組み込みの出力変数gl_FragColorにそのまま( \alpha値を1.0にして)、コピーしているだけです。
現在ではこのgl_FragColorは非推奨で、代わりに自分で定義した出力変数をフレームバッファに送る必要があるらしいです。

シェーダのコンパイルが終われば、プログラムオブジェクトを作成して、これまでに作成したシェーダをアタッチしてリンクします。
その後にOpenGLのパイプラインにこのプログラムをインストールします。

  GLuint programHandle = glCreateProgram();
  if (0 == programHandle)
  {
    std::cerr << "プログラムオブジェクトの作成でエラーがありました" << std::endl;
    std::exit(1);
  }
  glAttachShader(programHandle, vertShader);
  glAttachShader(programHandle, fragShader);

  glLinkProgram(programHandle);

  GLint status;
  glGetProgramiv(programHandle, GL_LINK_STATUS, &status);
  if (GL_FALSE == status)
  {
    std::cerr << "シェーダプログラムのリンクに失敗しました" << std::endl;
    std::exit(1);
  }

  glUseProgram(programHandle);

そのままなので特に書くこともないです。

今回使用する頂点シェーダは、アプリケーションプログラムからVertexPositionと、VertexColorという名前の入力変数を受け取ります。
これらの変数に値を流し込むためには、変数のindexを取得する必要があります。
これには2つの方法があるらしいです。

  • indexの決定をOpenGL(glLinkProgram)にまかせて、リンクの後にglGetAttribLocationを使ってそのindexを得る。
  • リンク前にglBindAttribLocationを使って明示的に変数のindexを指定する。そのindexを使う。

他にもシェーダの中でレイアウト修飾子を使って属性のインデックスを定義することもできる(ようになった?)らしいです。
自分のパソコンのOpenGLでそれが出来るのかは試していないのでわかりません。

今回はプログラムのリンク前にindexの明示的な指定を行っていないので、前者の方法を使うことになります。

  GLint attributeVertexPosition;
  GLint attributeVertexColor;
  attributeVertexPosition =
      glGetAttribLocation(programHandle, "VertexPosition");
  attributeVertexColor =
      glGetAttribLocation(programHandle, "VertexColor");

VBO(vertex buffer object、頂点バッファオブジェクト)の作成を行います。

  GLuint vboTriangleHandles[2];
  glGenBuffers(2, vboTriangleHandles);

  GLfloat positionData[] = {
    -0.8f, -0.8f,  0.0f,
     0.8f, -0.8f,  0.0f,
     0.0f,  0.8f,  0.0f
  };
  GLfloat colorData[] = {
    1.0f, 0.0f, 0.0f,
    0.0f, 1.0f, 0.0f,
    0.0f, 0.0f, 1.0f
  };
  
  glBindBuffer(GL_ARRAY_BUFFER, vboTriangleHandles[0]);
  glBufferData(GL_ARRAY_BUFFER, sizeof(positionData),
               positionData, GL_STATIC_DRAW);
  glBindBuffer(GL_ARRAY_BUFFER, vboTriangleHandles[1]);
  glBufferData(GL_ARRAY_BUFFER, sizeof(colorData),
               colorData, GL_STATIC_DRAW);

まず最初にglGenBuffersでバッファオブジェクトを作成しています。
そのハンドルをvboTriangleHandlesに格納しています。

次にglBindBufferを呼び出して、GL_ARRAY_BUFFERバインディングポイントに先ほど作ったバッファオブジェクト(vboTriangleHandle[0]をハンドルとする)をバインドしています。
その後にglBufferDataを呼び出し、positionDataをそのバッファオブジェクトに流し込んでいます。

glBufferDataは第一引数のバインディングポイント、第二引数に流し込みたい配列のサイズ(バイト数)、第三引数にその配列の先頭要素のアドレス、最後の引数にOpenGLに与える、データの使い方についてのヒントです。
最初の引数で指定されたバインディングポイントに現在バインドされているバッファオブジェクトに第三引数で供給されるデータ(サイズは第二引数で指定される)を(第四引数で指定されるヒントをOpenGLに伝えるとともに)流しこむっていう感じですかね。
よくわからないです。

同様のことをcolorDataにも行います。

次にVAO(vertex array object、頂点配列オブジェクト)作成します。
VAOのハンドルとしてグローバルにvaoHandle(型はGLuint)を定義しておきます。

  glGenVertexArrays(1, &vaoHandle);
  glBindVertexArray(vaoHandle);

VAOはバッファ中のデータと入力頂点属性の間の接続についての情報を保持するらしいです。
描画する際、頂点シェーダにバッファ中のデータを送るわけですが、当然どのデータをどの入力頂点属性に流しこめばいいのかを指定する必要があるわけです。
VAOはこの接続の情報を保持してくれるため、一回一回の明示的な接続を行う必要がなくなり、描画処理のためのコードが比較的短く済むようになります。

まず、glGenVertexArraysを呼び出してVAOを作成し、ハンドルをグローバル変数vaoHandleに格納しています。
そしてglBindVertexArrayを呼び出して、先ほど作成したVAOをバインドしています。

次に頂点属性配列を有効にし、バッファオブジェクトと頂点属性間の接続を行います。

  glEnableVertexAttribArray(attributeVertexPosition);
  glEnableVertexAttribArray(attributeVertexColor);

  glBindBuffer(GL_ARRAY_BUFFER, vboTriangleHandles[0]);
  glVertexAttribPointer(attributeVertexPosition, 3,
                        GL_FLOAT, GL_FALSE, 0, 0);

  glBindBuffer(GL_ARRAY_BUFFER, vboTriangleHandles[1]);
  glVertexAttribPointer(attributeVertexColor, 3,
                        GL_FLOAT, GL_FALSE, 0, 0);

glEnableVertexAttribArrayで先ほど調べた、位置がattributeVertexPositionとattributeVertexColorである属性を有効にします。
これにより属性の値が、アクセスされ、レンダリングで使われるようになるみたいです。

次にglBindBufferで(ハンドルがvboTriangleHandles[0]の)バッファをバインドし、その後にglVertexAttribPointerを使ってindexがattributeVertexPositionである頂点入力属性に(現在GL_ARRAY_BUFFERバインディングポイントにバインドしてある先ほどの)バッファを接続するわけです。
glVertexAttribPointerは最初の引数に入力属性のindexを渡し、第二引数に頂点属性あたりの要素の数(今はVertexPositionは三次元ベクトルなので3です)、第三引数にバッファの各要素のデータ型、第四引数にデータを自動的に正規化するかどうかを指定するブール値、第五引数と第六引数はよくわからないのですが、上手く使えばバッファを1つにまとめた状態で複数の頂点入力属性との接続が行えるみたいです。
今はとりあえず2つとも0しておけば問題ないみたいです。

同じようなことを色情報(VertexColor)にもします。

このようにVAOをバインドした状態でVBOと頂点属性の接続を設定するとそれをVAOは記憶するので、描画部分ではVAOについてのみ書けばよくなります。

以上で初期化関数InitializeGLは終わりです。

次に描画関数DisplayGLに行きます。

この関数は非常に短くてすみます(VAOを使ったこともあり)。

void DisplayGL()
{
  glClearColor(1.0, 1.0, 1.0, 1.0);
  glClear(GL_COLOR_BUFFER_BIT);
  
  glBindVertexArray(vaoHandle);
  glDrawArrays(GL_TRIANGLES, 0, 3);

  glutSwapBuffers();
}

glBindVertexArrayでハンドルがvalHandleのVAOをバインドし、その状態でglDrawArraysを呼び出しているだけですね。
書く必要も無いようなことですが(類推できるので)、glDrawArraysはバッファの有効な属性配列を順に読んで、データをパイプラインに流して頂点シェーダに渡し、プリミティブのレンダリングを起動してくれるわけです。

ではプログラム全体を表示します。

#include <GL/glew.h>
#include <GL/glut.h>
#include <iostream>
#include <cstdlib>

GLuint vaoHandle;

void InitializeGL();
void DisplayGL();

int main(int argc, char *argv[])
{
  glutInit(&argc, argv);
  glutInitWindowSize(500, 500);
  glutInitWindowPosition(10, 10);
  glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH);
  glutCreateWindow("OpenGL test01");

  InitializeGL();

  glutDisplayFunc(DisplayGL);
  glutMainLoop();
}

void InitializeGL()
{
  std::cout << "初期化処理を行います" << std::endl;
  std::cout << "GLEWの初期化" << std::endl;
  GLenum glewStatus = glewInit();
  if (glewStatus != GLEW_OK)
  {
    std::cerr << "Error: " << glewGetErrorString(glewStatus) << std::endl;
    std::exit(1);
  }
 
  std::cout << "頂点シェーダの設定" << std::endl;
  
  GLuint vertShader = glCreateShader(GL_VERTEX_SHADER);
  if (0 == vertShader)
  {
    std::cerr << "頂点シェーダ作成エラー" << std::endl;
    std::exit(1);
  }
  const GLchar* vertShaderCode =
      "#version 120\n"
      "attribute vec3 VertexPosition;"
      "attribute vec3 VertexColor;"
      "varying vec3 Color;"
      "void main() {"
      "Color = VertexColor;"
      "gl_Position = vec4(VertexPosition, 1.0);"
      "}";
  glShaderSource(vertShader, 1, &vertShaderCode, NULL);
  glCompileShader(vertShader);

  GLint result;
  glGetShaderiv(vertShader, GL_COMPILE_STATUS, &result);
  if (GL_FALSE == result)
  {
    std::cerr << "頂点シェーダのコンパイルに失敗しました" << std::endl;
    std::exit(1);
  }

  std::cout << "フラグメントシェーダの設定" << std::endl;
  GLuint fragShader = glCreateShader(GL_FRAGMENT_SHADER);
  if (0 == fragShader)
  {
    std::cerr << "フラグメントシェーダ作成エラー" << std::endl;
    std::exit(1);
  }
  const GLchar* fragShaderCode =
      "#version 120\n"
      "varying vec3 Color;"
      "void main() {"
      "gl_FragColor = vec4(Color, 1.0);"
      "}";
  glShaderSource(fragShader, 1, &fragShaderCode, NULL);
  glCompileShader(fragShader);

  glGetShaderiv(fragShader, GL_COMPILE_STATUS, &result);
  if (GL_FALSE == result)
  {
    std::cerr << "フラグメントシェーダのコンパイルに失敗しました" << std::endl;
    std::exit(1);
  }

  std::cout << "シェーダプログラムの設定" << std::endl;
  GLuint programHandle = glCreateProgram();
  if (0 == programHandle)
  {
    std::cerr << "プログラムオブジェクトの作成でエラーがありました" << std::endl;
    std::exit(1);
  }
  glAttachShader(programHandle, vertShader);
  glAttachShader(programHandle, fragShader);

  glLinkProgram(programHandle);

  GLint status;
  glGetProgramiv(programHandle, GL_LINK_STATUS, &status);
  if (GL_FALSE == status)
  {
    std::cerr << "シェーダプログラムのリンクに失敗しました" << std::endl;
    std::exit(1);
  }

  glUseProgram(programHandle);

  GLint attributeVertexPosition;
  GLint attributeVertexColor;
  attributeVertexPosition =
      glGetAttribLocation(programHandle, "VertexPosition");
  attributeVertexColor =
      glGetAttribLocation(programHandle, "VertexColor");

  GLuint vboTriangleHandles[2];
  glGenBuffers(2, vboTriangleHandles);

  GLfloat positionData[] = {
    -0.8f, -0.8f,  0.0f,
     0.8f, -0.8f,  0.0f,
     0.0f,  0.8f,  0.0f
  };
  GLfloat colorData[] = {
    1.0f, 0.0f, 0.0f,
    0.0f, 1.0f, 0.0f,
    0.0f, 0.0f, 1.0f
  };
  
  glBindBuffer(GL_ARRAY_BUFFER, vboTriangleHandles[0]);
  glBufferData(GL_ARRAY_BUFFER, sizeof(positionData),
               positionData, GL_STATIC_DRAW);
  glBindBuffer(GL_ARRAY_BUFFER, vboTriangleHandles[1]);
  glBufferData(GL_ARRAY_BUFFER, sizeof(colorData),
               colorData, GL_STATIC_DRAW);
  
  glGenVertexArrays(1, &vaoHandle);
  glBindVertexArray(vaoHandle);
  
  glEnableVertexAttribArray(attributeVertexPosition);
  glEnableVertexAttribArray(attributeVertexColor);

  glBindBuffer(GL_ARRAY_BUFFER, vboTriangleHandles[0]);
  glVertexAttribPointer(attributeVertexPosition, 3,
                        GL_FLOAT, GL_FALSE, 0, 0);

  glBindBuffer(GL_ARRAY_BUFFER, vboTriangleHandles[1]);
  glVertexAttribPointer(attributeVertexColor, 3,
                        GL_FLOAT, GL_FALSE, 0, 0);
}

void DisplayGL()
{
  glClearColor(1.0, 1.0, 1.0, 1.0);
  glClear(GL_COLOR_BUFFER_BIT);
  
  glBindVertexArray(vaoHandle);
  glDrawArrays(GL_TRIANGLES, 0, 3);

  glutSwapBuffers();

}

実行結果は以下のようになります。
f:id:Code-C:20140320140644p:plain

初期化関数長すぎですね。。。
う〜ん。
ここまで色々と調べながらプログラムを書いてみたけど、めっちゃ難しいですね、OpenGL
三角形を表示するプログラムなんて、いわゆるHelloWorld的なプログラムなんだと思いますが、そう考えると道のりの長さに目眩がしてきますね。

次からはこのプログラムをいじって実験していこうと思います。