A Day In The Life

とあるプログラマの備忘録

OpenGL ES入門 その2 -三角形の描画とシェーダーの仕組み-

OpenGL ES入門 その1 -描画の仕組みとバッファ-の続きです。今回は OpenGL ES を使って三角形を描画してみます。三角形を描くだけなら簡単そうな気もしますが、三角形を描くにはシェーダーを用意しきゃいけないという少し面倒くさい作業があるのでその辺の説明になります。

OpenGL は三角形以上の多角形を描画することができない

前回も説明しましたが、OpenGL が描画できるのは点、線、三角形だけです。多角形は三角形の集合で構成されます。三角形以上のN角形は以下のように N-2 個の三角形によって構成されます。
N-1個の三角形

三角形の描画手順

OpenGL は頂点間を塗りつぶして描画を行います。頂点は 3D 空間上に配置することができます。
三角形の描画
もう少し詳しく説明すると、プログラム上で描画したい三角形の頂点を決めます。決まった頂点データを頂点シェーダーに渡して表示するデバイスの位置を計算します。デバイスに表示する位置が決まったらラスタライズという処理がされて三角形の塗りつぶしに必要なピクセルが決まります。使用するピクセルの位置が決まったら、そのピクセルのデータを使ってフラグメントシェーダーがピクセルの色を決定します。最後に決定された色で三角形を描画します。
ここでシェーダーという聞きなれない言葉が出てきました。シェーダーとは一体なんなんでしょう。

三角形描画にはシェーダーが必要

シェーダーとは描画処理の一部(主に陰影処理=シェーディング)を GPU を使って処理するための専用プログラムです。OpenGL ES 2.0で使用することができるシェーダーは以下の2種類があります*1

  • 頂点シェーダー(バーテックスシェーダー)
    頂点入力を座標変換するためのシェーダー。頂点の最終的な座標を決める
  • フラグメントシェーダー(ピクセルシェーダー)
    ピクセル単位のライティングや後処理を行うためのシェーダー。頂点シェーダーから入力された情報をもとにテクスチャを合成したり表面色を決める

シェーダーのプログラムは GLSL ES(OpenGL Shader Language ES)というC言語に似た専用の言語で記述します。シェーダーは GPU が並列で実行してくれます。シェーダーを使うための手順は以下のようになります。

  1. 頂点シェーダー用プログラムを作成する
  2. フラグメントシェーダー用プログラムを作成する
  3. 2つのシェーダープログラムをコンパイルする
  4. コンパイルしたプログラムをリンクする
  5. シェーダーを利用する

上記の3以降はプログラム実行時に行います。OpenGL にシェーダーのコンパイルとリンク用の関数が用意されているのでそれを使います。

頂点シェーダーのプログラム

それでは初めに頂点シェーダーのプログラムを見てみましょう。以下は三角形描画に必要な頂点シェーダーのプログラムです。

attribute mediump vec4 pos;
void main() {
  gl_Position = pos;
}

1行目で頂点情報を格納する変数の宣言をしています。pos が変数名です。前についている修飾子や型名の意味は以下のとおりです。

  • attribute
    頂点属性の宣言
  • mediump
    演算精度の指定。mediump は中精度、他に高精度の highp と低精度の lowp がある
  • vec4
    変数の型の指定、vec4 は x, y, z, w の4要素を持つ構造体型

main 関数内の gl_Position は OpenGL の組み込み変数です。gl_Position に頂点の情報を渡しています。

頂点シェーダーはどのように実行されるのか

頂点シェーダーは描画に利用される頂点の数だけ実行されます。例えば三角形1つを描画する場合は頂点が3つなので3回実行されます。
頂点シェーダー

フラグメントシェーダーのプログラム

次にフラグメントシェーダーのプログラムを見てみましょう。以下は三角形描画に必要なフラグメントシェーダーのプログラムです。

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

gl_FragColor は OpenGL の組み込み変数です。gl_FragColor に色情報を渡しています(プログラム例だと赤色になります)。

フラグメントシェーダーはどのように実行されるのか

頂点シェーダーから渡された頂点データは GPU によって三角形に結ばれて中身の塗りつぶしが始まります。その時どんな色で塗るのかをピクセルシェーダーによって判断します。頂点シェーダーは頂点の数だけ実行されましたが、フラグメントシェーダーは塗りつぶされるピクセル数分実行されます。例えば100px × 100pxの図形の場合、100 × 100 = 1万回実行されます。
フラグメントシェーダー

シェーダーのコンパイルとリンク

シェーダーを使用するにはコンパイルとリンクが必要です。これらの処理はプログラム内で行います。初めに頂点シェーダーをコンパイルするプログラムを見てみましょう。以下は頂点シェーダーをコンパイルするプログラムの例です。コンパイルとリンクの処理はビューの初期化処理のタイミングで行います。

// 頂点シェダーオブジェクト生成
GLuint vertShader = glCreateShader(GL_VERTEX_SHADER);
// 頂点シェーダーソースコード
const GLchar *vertShaderSource =
    "attribute mediump vec4 pos;"
    "void main() {"
    "  gl_Position = pos;"
    "}";
// シェーダーオブジェクトとソースコードを結びつける
glShaderSource(vertShader, 1, &vertShaderSource, NULL);
// GLSLのコンパイル
glCompileShader(vertShader);

シェーダーのソースコードは手抜きして文字列で書いてますが複雑になる場合は別途ファイルを用意してファイルから読み込むようにした方が良いでしょう。同じようにフラグメントシェーダーもコンパイルしてみます。

// フラグメントシェダーオブジェクト生成
GLuint fragShader = glCreateShader(GL_FRAGMENT_SHADER);
// フラグメントシェーダーソースコード
const GLchar *fragShaderSource =
    "void main() {"
    "  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);"
    "}";
// シェーダーオブジェクトとソースコードを結びつける
glShaderSource(fragShader, 1, &fragShaderSource, NULL);
// GLSLのコンパイル
glCompileShader(fragShader);

使う関数は頂点シェーダーのコンパイルの時と同じです。引数が違うのとソースがフラグメントシェーダーに変わっているだけです。シェーダーのコンパイル処理は共通なので以下のような関数を作成しておくと良いと思います。

GLuint compileShader(GLuint shaderType, const GLchar *source)
{
  // シェダーオブジェクト生成
  GLuint shader = glCreateShader(shaderType);
  // シェーダーオブジェクトとソースコードを結びつける
  glShaderSource(shader, 1, &source, NULL);
  // GLSLのコンパイル
  glCompileShader(shader);
  return shader;
}

続いてコンパイルしたプログラムのリンクです。リンクが終わったらシェーダーオブジェクトを解放するのを忘れないようにしましょう。

// プログラムオブジェクトの生成
GLuint shaderProgram = glCreateProgram();
// 頂点シェーダーとプログラムオブジェクトを結びつける
glAttachShader(shaderProgram, vertShader);
// フラグメントシェーダーとプログラムオブジェクトを結びつける
glAttachShader(shaderProgram, fragShader);
// リンク
glLinkProgram(shaderProgram);
// シェーダーオブジェクトの解放
glDeleteShader(vertShader);
glDeleteShader(fragShader);

シェーダーのコンパイルとリンクができたのでシェーダーの利用を開始します。シェーダーの利用開始には以下のように glUseProgram 関数を使用します。

// シェーダーの利用を開始する
glUseProgram(shaderProgram);

三角形を描画する

シェーダーの準備ができたので、三角形を描いてみます。三角形の描画は以下の手順でゲームループの中で行います。

  1. 頂点シェーダーに宣言されている頂点用変数を取得する
  2. 取得した頂点用変数へのアクセスを可能にする
  3. 頂点データを作成する
  4. 頂点用変数に頂点データを渡す
  5. 三角形を描画する

プログラムにすると以下のようになります。

- (void)update:(NSTimer *)timer
{
  // 頂点シェーダーのpos変数の位置を取得する
  GLint posLocation = glGetAttribLocation(shaderProgram, "pos");
  // pos変数へのアクセスを有効にする
  glEnableVertexAttribArray(posLocation);
  // 頂点データ
  const GLfloat vertex[] = {
    0.0f, 0.5f,
    -0.5f, -0.5f,
    0.5f, -0.5f,
  };
  // pos変数に頂点データを渡す
  glVertexAttribPointer(posLocation, 2, GL_FLOAT, GL_FALSE, 0, vertex);
  // 描画する
  glDrawArrays(GL_TRIANGLES, 0, 3);
  [_context presentRenderbuffer:GL_RENDERBUFFER];
}

三角形を描画するには頂点シェーダーに三角形の頂点データを渡す必要があります。さきほど作成した頂点シェーダーでは pos という名前の変数を宣言しました。念のためもう一度頂点シェーダーのプログラムを確認しておきましょう。

// 頂点用変数
attribute mediump vec4 pos;
void main() {
  gl_Position = pos;
}

この pos 変数には値が設定されていませんので OpenGL のプログラムからこの変数にアクセスして実際に描画したい三角形の頂点データをセットしてやります。pos 変数にアクセスするには glGetAttribLocation 関数を使います。この関数を使うと pos 変数の attribute location と呼ばれる ID が取得できます。次に glEnableVertexAttribArray 関数を使って pos 変数へのアクセスを有効にします。pos 変数へのアクセスが有効になったら glVertexAttribPointer 関数を使って pos 変数に頂点データを渡します。最後に glDrawArrays 関数を使って三角形の描画処理を行います。

シェーダープログラムの解放

シェーダープログラムは使い終わったらメモリから解放する必要があります。解放処理は UIView の dealloc メソッドに書きます。

- (void)dealloc
{
  // シェーダーの利用を終了する
  glUseProgram(0);
  // シェーダープログラムの解放
  glDeleteProgram(_shaderProgram);
}

ここまでのプログラムをまとめると

そこそこ複雑なプログラムになったのでここで全体のプログラムを見てみましょう。以下は前回の記事のソースコードに修正を加えて三角形を描画するためのプログラムです。

// シェーダーをコンパイルする関数
GLuint compileShader(GLuint shaderType, const GLchar *source)
{
  // シェダーオブジェクト生成
  GLuint shader = glCreateShader(shaderType);
  // シェーダーオブジェクトとソースコードを結びつける
  glShaderSource(shader, 1, &source, NULL);
  // GLSLのコンパイル
  glCompileShader(shader);
  return shader;
}

@implementation GLSurfaceView {
  EAGLContext *_context;
  GLuint _shaderProgram;
}
+ (Class)layerClass {
  return [CAEAGLLayer class];
}
// ストーリーボードから呼ばれるイニシャライザ
- (instancetype)initWithCoder:(NSCoder*)coder
{
  if ((self = [super initWithCoder:coder])) {
    [self setUp];
    // ゲームループ
    [NSTimer scheduledTimerWithTimeInterval:1/60.0f target:self selector:@selector(update:) userInfo:nil repeats:YES];
  }
  return self;
}
- (void)setUp
{
  // レイヤーのセットアップ
  CAEAGLLayer *layer = (CAEAGLLayer*)self.layer;
  layer.opaque = YES;
  // コンテキストオブジェクトのセットアップ
  _context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
  [EAGLContext setCurrentContext:_context];
  // レンダーバッファーのセットアップ
  GLuint renderBuffer;
  glGenRenderbuffers(1, &renderBuffer);
  glBindRenderbuffer(GL_RENDERBUFFER, renderBuffer);
  [_context renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer];
  // フレームバッファーのセットアップ
  GLuint frameBuffer;
  glGenFramebuffers(1, &frameBuffer);
  glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);
  glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderBuffer);
  
  // 頂点シェーダーソースコード
  const GLchar *vertShaderSource =
      "attribute mediump vec4 pos;"
      "void main() {"
      "  gl_Position = pos;"
      "}";
  // 頂点シェダーオブジェクト生成
  GLuint vertShader = compileShader(GL_VERTEX_SHADER, vertShaderSource);
  
  // フラグメントシェーダーソースコード
  const GLchar *fragShaderSource =
      "void main() {"
      "  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);"
      "}";
  // フラグメントシェダーオブジェクト生成
  GLuint fragShader = compileShader(GL_FRAGMENT_SHADER, fragShaderSource);
  
  // プログラムオブジェクトの生成
  _shaderProgram = glCreateProgram();
  // 頂点シェーダーとプログラムオブジェクトを結びつける
  glAttachShader(_shaderProgram, vertShader);
  // フラグメントシェーダーとプログラムオブジェクトを結びつける
  glAttachShader(_shaderProgram, fragShader);
  // リンク
  glLinkProgram(_shaderProgram);
  
  // シェーダーオブジェクトの解放
  glDeleteShader(vertShader);
  glDeleteShader(fragShader);

  // シェーダーの利用を開始する
  glUseProgram(_shaderProgram);
}
- (void)layoutSubviews
{
  // ViewPortの設定
  glViewport(0, 0, self.frame.size.width, self.frame.size.height);
}
- (void)update:(NSTimer *)timer
{
  // 頂点シェーダーのpos変数の位置を取得する
  GLint posLocation = glGetAttribLocation(_shaderProgram, "pos");
  // pos変数へのアクセスを有効にする
  glEnableVertexAttribArray(posLocation);
  // 頂点データ
  const GLfloat vertex[] = {
    0.0f, 0.5f,
    -0.5f, -0.5f,
    0.5f, -0.5f,
  };
  // 頂点データを作成する
  glVertexAttribPointer(posLocation, 2, GL_FLOAT, GL_FALSE, 0, vertex);
  // 描画する
  glDrawArrays(GL_TRIANGLES, 0, 3);
  
  [_context presentRenderbuffer:GL_RENDERBUFFER];
}
- (void)dealloc
{
  // シェーダーの利用を終了する
  glUseProgram(0);
  // シェーダープログラムの解放
  glDeleteProgram(_shaderProgram);
}
@end

layoutSubviews メソッドで Viewport の設定をしています。Viewport については次の機会に詳しく説明します。

実行結果

ここまでのプログラムを実行すると以下のようになります。

サンプルプログラム

今回作成したサンプルプログラムをこちらに置いておきます。

次回は

次回は Viewport と OpenGL の座標系について説明します。

参考図書

*1:本家 OpenGLOpenGL ES 3.2以降はここで紹介した2つに加えてジオメトリシェーダーを使うことができます