CDR6275

Michio SHIRAISHI Official Site


OpenGL 4.1とOpenGL ES SL 1.0で学ぶ3次元コンピュータグラフィックス

東邦大学理学部情報科学科 白石路雄
最終更新: 2014年2月20日

2. 最初のプログラム

 この章では三角形をウィンドウに表示するプログラムを通して、基本的な部分について説明します。

2.2 シェーダを使って三角形を描く

 基本的な図形を描くプログラムをプロジェクト「FirstShader」に作ってみました。1つのソースプログラムFirstStartMain.cppにすべて書きました。

 申し訳ないのですが、ちょっと長くなってしまいました。ただ、多くの部分は「お約束」なので、村上春樹さんも言っているように、そういうものだ、と思っていただくのがよいと思います。後ほど、お約束の部分ははしょりつつ、重要なところだけ説明しますので、ご安心ください。では、実行結果とソースコードです。

first-shader
#include <iostream>

#if defined _WIN32
#include <GL/glew.h>
#pragma comment(lib, "glew32.lib")
#pragma comment(lib, "glfw3dll.lib")
#pragma comment(lib, "opengl32.lib")
#endif

#if defined __APPLE__
#define GLFW_INCLUDE_GLCOREARB
#define GL3_PROTOTYPES
#endif
#include <GLFW/glfw3.h>

// GLFWでエラーとなったときに呼び出される関数
void glfw_error_callback_func(int error, const char* description){
  std::cerr << "GLFW Error: " << description << std::endl;
}

// 三角形の3つの頂点の座標
static float faceVertexCoordinates[] = {
  0.0f, 0.0f, 0.0f,
  1.0f, 0.0f, 0.0f,
  0.0f, 1.0f, 0.0f,
};

// 頂点シェーダのコード
const char* vertexShaderCode = "" \
"#version 100\n" \
"attribute vec3 vertexCoordinate;\n" \
"void main(){\n" \
"	 gl_Position = vec4(vertexCoordinate, 1.0);\n" \
"}\n";

// シェーダのattribute変数に設定されるインデックス
enum
{
  ATTRIBUTE_VERTEX_COORDINATE,
  NUM_ATTRIBUTES
};

// フラグメントシェーダのコード
const char* fragmentShaderCode = "" \
"#version 100\n" \
"void main(){\n" \
"  gl_FragColor = vec4(0.0, 0.0, 1.0, 1.0);\n" \
"}\n";

int main(void){
  // GLFWでエラーとなったときに呼び出される関数の設定
	glfwSetErrorCallback(glfw_error_callback_func);

  // GLFWの初期化
  if(!glfwInit()){
    std::cerr << "glfwInit failed." << std::endl;
    return EXIT_FAILURE;
  }
  
  // 使用するOpenGLのバージョン(4.1 Compatibility Mode)の指定
  glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
  glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1);
  glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
  glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);

  // ウィンドウとOpenGLコンテキストの作成
  GLFWwindow* window = glfwCreateWindow(640, 640, "FirstShader", NULL, NULL);
  if(!window){
    std::cerr << "glfwCreateWindow failed." << std::endl;
    glfwTerminate();
    return NULL;
  }
  
  // 現在のウィンドウに描くようにカレントコンテキストを設定
  glfwMakeContextCurrent(window);
  
  // GLEWの初期化
#if defined _WIN32
  glewExperimental=GL_TRUE;
  if(glewInit()!=GLEW_OK){
    std::cerr << "glewInit failed." << std::endl;
    return EXIT_FAILURE;
  }
#endif

  // OpenGLのバージョンチェック
  std::cerr << "GL_VERSION: "  <<  glGetString(GL_VERSION) << std::endl;
  std::cerr << "GL_SHADING_LANGUAGE_VERSION: " <<  glGetString(GL_SHADING_LANGUAGE_VERSION) << std::endl;
  std::cerr << "GL_VENDOR: "   <<  glGetString(GL_VENDOR) << std::endl;
  std::cerr << "GL_RENDERER: " <<  glGetString(GL_RENDERER) << std::endl;

    // 頂点シェーダオブジェクトの作成
  GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
  glShaderSource(vertexShader, 1, &vertexShaderCode, NULL);
  glCompileShader(vertexShader);

  // 頂点シェーダのコンパイルが正常に行えたかどうかのチェック
  GLint vertexShaderCompileStatus;
  glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &vertexShaderCompileStatus);
  if(vertexShaderCompileStatus == GL_FALSE){
    std::cerr << "Vertex Shader Compile Error: ";
    GLsizei logLength, logLengthWritten;
    glGetShaderiv(vertexShader, GL_INFO_LOG_LENGTH , &logLength);    
    GLchar *logString = new GLchar[logLength];
    glGetShaderInfoLog(vertexShader, logLength, &logLengthWritten, logString);
    std::cerr << logString << std::endl;
    delete[] logString;
  }

  // フラグメントシェーダオブジェクトの作成
  GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
  glShaderSource(fragmentShader, 1, &fragmentShaderCode, NULL);
  glCompileShader(fragmentShader);

  // フラグメントシェーダのコンパイルが正常に行えたかどうかのチェック
  GLint fragmentShaderCompileStatus;
  glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &fragmentShaderCompileStatus);
  if(fragmentShaderCompileStatus == GL_FALSE){
    std::cerr << "Fragment Shader Compile Error." << std::endl;
    GLsizei logLength, logLengthWritten;
    glGetShaderiv(fragmentShader, GL_INFO_LOG_LENGTH , &logLength);
    GLchar *logString = new GLchar[logLength];
    glGetShaderInfoLog(fragmentShader, logLength, &logLengthWritten, logString);
    std::cerr << logString << std::endl;
    delete[] logString;
  }
  
  // プログラムオブジェクトの作成
  GLuint program = glCreateProgram();
  
  // コンパイルした頂点シェーダとフラグメントシェーダをプログラムオブジェクトに結びつける
  glAttachShader(program, vertexShader);
  glAttachShader(program, fragmentShader);
  
  // シェーダコード内の変数にインデックスを設定する
  glBindAttribLocation(program, ATTRIBUTE_VERTEX_COORDINATE, "vertexCoordinate");

  // プログラムオブジェクトのリンク
  glLinkProgram(program);

  // リンクが正常に行えたかどうかのチェック
  GLint linkStatus;
  glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);
  if(linkStatus == GL_FALSE){
    std::cerr << "Program Link Error." << std::endl;
    GLsizei logLength, logLengthWritten;
    glGetProgramiv(program, GL_INFO_LOG_LENGTH, &logLength);
    GLchar *logString = new GLchar[logLength];
    glGetProgramInfoLog(program, logLength, &logLengthWritten, logString);
    std::cerr << logString << std::endl;
    delete[] logString;
  }
 
  // 頂点シェーダをリリースする
  if(vertexShader){
    glDetachShader(program, vertexShader);
    glDeleteShader(vertexShader);
  }

  // フラグメントシェーダをリリースする
  if(fragmentShader){
    glDetachShader(program, fragmentShader);
    glDeleteShader(fragmentShader);
  }
  
  // 頂点配列オブジェクトを作成して設定する
  GLuint vertexArrayObject;
  glGenVertexArrays(1, &vertexArrayObject);
  glBindVertexArray(vertexArrayObject);
  
  // 頂点バッファオブジェクトを作成する
  GLuint vertexBufferObject;
  glGenBuffers(1, &vertexBufferObject);
  glBindBuffer(GL_ARRAY_BUFFER, vertexBufferObject);
  glBufferData(GL_ARRAY_BUFFER, sizeof(float)*9, faceVertexCoordinates, GL_STATIC_DRAW);

  // 頂点バッファオブジェクトにシェーダ内の変数vertexCoodrinateを結びつける
  glEnableVertexAttribArray(ATTRIBUTE_VERTEX_COORDINATE);
  glVertexAttribPointer(ATTRIBUTE_VERTEX_COORDINATE, 3, GL_FLOAT, GL_FALSE, sizeof(float)*3, 0);

  // 背景色の設定
  glClearColor(0.75f, 0.75f, 0.75f, 1.0f);

  // ユーザがウィンドウを閉じるまでループする
  while (!glfwWindowShouldClose(window)){
    // 背景のクリア
    glClear(GL_COLOR_BUFFER_BIT);
    // 使用するプログラムオブジェクトをする
    glUseProgram(program);
    // 三角形を描く
    glDrawArrays(GL_TRIANGLES, 0, 3);
    // 使用するプログラムオブジェクトを解除する
    glUseProgram(0);
    // バッファの入れ替え
    glfwSwapBuffers(window);
    // イベントの取得
    glfwPollEvents();
  }
  // プログラムオブジェクトの後処理
  glDeleteProgram(program);

  // GLFWの終了
  glfwTerminate();
  return EXIT_SUCCESS;
}

 さて、このプログラムを実行すると、次のような結果が得られるはずです。

 まずは#で始まるプリプロセッサの部分の後に三角形の3つの頂点の座標を並べてあります。点1のx座標、点1のy座標、点1のz座標、点2のx座標、点2のy座標、…、の順で全部で9つの数値が入っています。

 次に頂点シェーダのプログラムの文字列、頂点シェーダに関係する定数の定義、フラグメントシェーダのプログラムの文字列があります。後ほど詳しくやりますので、いまのところはスルーしておきましょう。

 で、いよいよmain関数が来るわけですが、最初の方はお約束ですので、これもスルーしておきましょう。「頂点シェーダオブジェクトの作成」というあたりからが本題です。

シェーダとはどのようなものか

 3次元コンピュータグラフィックスのプログラムの目的は、頂点の座標や面の性質などのデータから、画像を構成するピクセルにおける色を計算することです。この計算を行うプログラムをシェーダと言います。シェーダには頂点シェーダとフラグメントシェーダの2種類があり、後ほど詳しく説明します。

 シェーダはシェーダ用の言語で記述します。この資料では「OpenGL ES Shading Language 1.0」と呼ばれるものを使っています。この言語はiOSやAndroidのアプリケーションで使われているOpenGL ES 2.0で使用されているものです。上で頂点シェーダとフラグメントシェーダのコードがC++言語の文字列として記述されていましたが、その1行目の「#version 100」が、「このシェーダプログラムはOpenGL ES Shading Language 1.0で書かれている」ことを宣言しています。

 シェーダは基本的には実行時にGPU上でコンパイルされて利用されます。具体的には次の順序でOpenGLのコマンドを呼び出していきます。

1. シェーダをコンパイルする

 まずシェーダをコンパイルします。シェーダには頂点シェーダとフラグメントシェーダの2つがあることを述べました。以下に頂点シェーダをコンパイルしている部分を再掲します。

  GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
  glShaderSource(vertexShader, 1, &vertexShaderCode, NULL);
  glCompileShader(vertexShader);

 まずglCreateShader関数を呼び出して、空のシェーダオブジェクトを作成します。頂点シェーダをコンパイルするときには引数としてGL_VERTEX_SHADERを与え、フラグメントシェーダをコンパイルするときには引数としてGL_FRAGMENT_SHADERを与えます。この関数の戻り値を使って、これ以降のプログラムにおいて頂点シェーダオブジェクトを参照します。

 ここで分かるようにOpenGL APIに含まれる関数には、接頭辞として「gl」というのがつきます。また、「GL_」で始まるトークンもOpenGLが提供しているものです。OpenGLを使うプログラムを書くときには、これらの名前を使わないようにしましょう。

 glCreateShaderでシェーダオブジェクトを作った後に、シェーダオブジェクトで使用するシェーダのコードを設定します。そのための関数がglShaderSourceです。この関数の最初の引数は先ほど作成したシェーダオブジェクトを参照するための値を指定します。2番目の引数にはシェーダコードの数を指定しますが、ここでは1つだけしか使用しないので1にしておきましょう。3番目の引数は、複数の文字列を渡せるようにconst GLchar**型となっていますが、1つだけしか使用しないので文字列のアドレスを渡しておけばよいです。最後の引数については文字列の長さを与えることになっていますが、NULLで終わる普通の文字列であれば、NULLを渡しておけばよいです。

 最後にglCompileShaderでコンパイルしています。

 上で挙げたプログラムでは、シェーダのコンパイルが正常に行えたかどうかをチェックするためのプログラムが書いてあります。ここもそういうものだとお考えいただくのがよいかと思います。

 同様にして、フラグメントシェーダもコンパイルしておきます。

2. プログラムをリンクする

 コンパイルしたシェーダを利用するためには、プログラムオブジェクトと呼ばれるものを使ってリンクする必要があります。そのための手順が以下のようになります。

  GLuint program = glCreateProgram();
  glAttachShader(program, vertexShader);
  glAttachShader(program, fragmentShader);
  glBindAttribLocation(program, ATTRIBUTE_VERTEX_COORDINATE, "vertexCoordinate");
  glLinkProgram(program);

 この部分では、まずプログラムオブジェクトを作成し(glCreateProgram)、2つのシェーダをプログラムオブジェクトに結びつけます(glAttachShader)。次に、頂点シェーダ内の変数vertexCoordinateATTRIBUTE_VERTEX_COORDINATE(つまり0)を指定しています。そして、最後にリンクしています。

 さて、「シェーダとはどのようなものか」というタイトルを掲げておきながら、2種類のシェーダ、すなわち、頂点シェーダとフラグメントシェーダの違いについては語ってきませんでした。次にその話になるかと思わせつつ、もうちょっとプログラムを眺めてみます。

描画の準備

 描画する前に、次の順番でデータをシェーダに送るための準備をします。

1. 頂点配列オブジェクトを作成する
 頂点配列オブジェクトとは、これから作成する頂点のありかなどを格納しておくところなのですが、とりあえず必要なので、作ります。
2. 頂点バッファオブジェクトを作成する
 ここで描画に使用する頂点のデータをバッファに格納します。手順としては、次の通りです。

  1. glGenBuffersでバッファオブジェクトを作ります。その結果、変数vertexBufferObjectにバッファオブジェクトを指定するための値が入ります。
  2. glBindBufferで、バッファに配列を確保するよという指示を行います。
  3. glBufferDataで、配列バッファにfaceVertexCoordinatesに入っている頂点の座標(9個)を格納します。
3. 頂点バッファオブジェクトにシェーダ内の変数vertexCoodrinateを結びつける

 ここで頂点バッファオブジェクトとシェーダ内の変数vertexCoodrinateを結びつけますが、そこでは、上のATTRIBUTE_VERTEX_COORDINATEを指定します。glVertexAttribPointerの引数は次のように6つあります。

void glVertexAttribPointer( GLuint index, 
  GLint size, 
  GLenum type, 
  GLboolean normalized,
  GLsizei stride,
  const GLvoid * pointer);

この関数を次のようにして使っています。

glVertexAttribPointer(ATTRIBUTE_VERTEX_COORDINATE, 3, GL_FLOAT, GL_FALSE, sizeof(float)*3, 0);

3つめにデータ型であるGL_FLOATを指定するところはわかりやすいですが、2つめの引数3ですが、これは頂点ごとに3つのデータを持つ、ということを意味しています。ここでは頂点ごとに3つの成分から持つデータを割り当てているので3になっています。たとえば、頂点にRGBAからなる色データを割り当てるときは4にします。

 また、5つめの引数には頂点ごとのデータのサイズを与えます。ここでは1つの頂点が3つのfloat型で表現されているので、sizeof(float)*3と書きます (注:この例では0でも動きますが、後ほど1つの頂点に座標データと色データを混ぜて与えることも考えてこのようにしています)。

メインループで描画する

 メインループは次のようになっています。

  while (!glfwWindowShouldClose(window)){
    // 背景のクリア
    glClear(GL_COLOR_BUFFER_BIT);
    // 使用するプログラムオブジェクトを設定する
    glUseProgram(program);
    // 三角形を描く
    glDrawArrays(GL_TRIANGLES, 0, 3);
    // 使用するプログラムオブジェクトを解除する
    glUseProgram(0);
    // バッファの入れ替え
    glfwSwapBuffers(window);
    // イベントの取得
    glfwPollEvents();
  }

 前回のプログラムと比較していると分かるように、このプログラムでは描画のコードが増えています。つまり、

    glUseProgram(program);
    glDrawArrays(GL_TRIANGLES, 0, 3);
    glUseProgram(0);

つまり、

  1. glUseProgram(program)として使用するシェーダプログラムを指定する
  2. 頂点バッファオブジェクトに入っているデータを描く
  3. glUseProgram(0)として使用するシェーダプログラムを解除する

ということを行っています。glDrawArraysの引数は、三角形として、頂点バッファオブジェクトの中で0番目から3個ぶんの三角形を描く、ということを意味しています。

 GL_TRIANGLESの部分には、それ以外にもGL_POINTSGL_LINE_STRIPGL_LINE_LOOPGL_LINESGL_TRIANGLE_STRIPGL_TRIANGLE_FANが指定できます (OpenGL ES 2.0ではこれだけが指定できますが、OpenGL 4ではさらに指定できるものがあります)。

さてシェーダを見てみる

プログラム全体が分かったところで、シェーダについて見てみることにしましょう。前に述べたようにシェーダには頂点シェーダとフラグメントシェーダという2つがあります。それぞれ次のような役割を持っています。

頂点シェーダ
与えられた頂点の座標を変換して、ウィンドウに描くための頂点の座標を計算するのが頂点シェーダの大きな役割の一つです。たとえば、描くオブジェクトを変形するときにはそれぞれの頂点座標を変換する必要がありますし、3次元空間の座標から2次元のウィンドウに合わせた座標に変換する必要があります。
フラグメントシェーダ
頂点シェーダによって変換された座標を元に描くべき三角形の頂点は決まりますが、三角形の内部を塗りつぶす役割を持っているのが、フラグメントシェーダです。

 さて、それではまず頂点シェーダのコードから見ていきましょう。上のプログラムではC言語における文字列としてシェーダのコードが書かれておりますが、中身の文字列は以下のようになっています。

#version 100
attribute vec3 vertexCoordinate;
void main(){
    gl_Position = vec4(vertexCoordinate, 1.0);
}

 ここで、最初の

#version 100

は、このシェーダのコードがOpenGL ES 2.0で使用されるOpenGL ES Shading Language 1.0という仕様に沿って書かれていることを意味しています。次の行

attribute vec3 vertexCoordinate;

では、メインのプログラムからシェーダのコードにデータを渡す指定をしています。vec3というデータ型で、変数名がvertexCoordinateのものを宣言しています。ここで、attributeという修飾子がついているので、この変数にはメインのプログラムからデータが渡されることになります。シェーダのメインのコードは次のようになっています。

void main(){
    gl_Position = vec4(vertexCoordinate, 1.0);
}

ここで、gl_Positionは、メインのプログラムからvertexCoordinateという変数を介して渡ってきた座標値を変換して代入します。このプログラムでは、4次元のベクトルデータであるgl_Positionに3次元のベクトルデータであるvertexCoordinateをコピーして、それだけでは4つめの成分が足りませんので、1.0を追加しています。つまり、やっていることは、

void main(){
    gl_Position = vec4(vertexCoordinate.x, vertexCoordinate.y, vertexCoordinate.z, 1.0);
}

ということと全く一緒です。

 この点はOpenGL ES Shading Languageのようなシェーダを書くための言語で特有の書き方です。つまり、基本的にはC言語のようなことができるわけですが、それに加えてコンピュータグラフィックスでよく用いられる便利な機能がいろいろと組み込まれています。ここではその詳細には触れませんが、後のステップで具体的な例を紹介していきたいと思います。

 また、フラグメントシェーダのコードは以下のようになっています。

#version 100
void main(){
    gl_FragColor = vec4(0.0, 0.0, 1.0, 1.0);
}

これも頂点シェーダと同様で、gl_FragColorに三角形を塗りつぶすための色を入れます。このプログラムでは固定色で、赤と緑の成分が0、青とアルファの成分が1の色を代入しています。

やってみよう

  1. 三角形を画面の端まで描くのではなく、ちょうど80%の大きさで描くためにはどのようにしたらいいですか?
  2. 三角形の色を黄色にするためにはどのようにしたらいいですか?