A Day In The Life

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

OpenGL ES入門 その1 -描画の仕組みとバッファ-

最近 Cocos2d-x を触っているとどうしても細かいところで OpenGL の知識がないと理解できないみたいなことが多いです。いままで避けてきた感がある OpenGL の勉強をそろそろ始めてみようかなと思い少しづつ日記的にまとめていこうと思います。OpenGL と言っても自分はモバイルゲームの開発に興味があるので OpenGL ES について勉強していくことにしました。参考図書として購入した本はこちらです。

わりと新しく出た本ってのと解説がものすごくわかりやすかったので選びました。第1回は OpenGL ES の基本的なところから画面を色で塗りつぶすまで解説します。

OpenGL ES って何?

OpenGL は三角形以下(三角形、線、点)の図形を3D空間に高速に描画するための API です。Ope GL ES(OpenGL for Embedded Systems) は主にモバイル端末向けに OpenGL の冗長な機能を取り除いた簡略版です。

OpenGL ES のバージョン

  • 1.1
    基本的な固定機能グラフィックスパイプラインを提供する
  • 2.0
    プログラマブルシェイダを使用することができる。1.0 との互換性はない
  • 3.0
    iOS7以降で使用することができる。マルチレンダーターゲット機能やマルチサンプルアンチエイリアス(MSAA)を標準サポート。バージョン2.0と互換性があり

本当は3.0の勉強をしようと思っていたのですが、3.0について解説している日本語の本が現状(2015/8現在)ないのと、2.0を学んでおけば3.0にも活かせるとこができる、というわけで2.0で進めます。

OpenGL で描画できる図形

OpenGL で描画できる図形は点、線、三角形の3つです。四角形以上の図形は三角形の組み合わせで描画します。

ゲームループ

OpenGL を使ったグラフィックの描画処理はゲームループの中で行います。ゲームループとは、ある一定時間ごとに呼び出される関数(またはメソッド)です。一般的なゲームでは1秒間に60回、関数が呼び出されます。これを60FPS(Frames Per Second)といいます。
ゲームループに関する詳しい仕組みや実装例なんかは以下の書籍(Game Programming Patterns)の第9章が参考になると思います。

ダブルバッファリング

OpenGL はダブルバッファリングという手法を使って描画処理を行います。ダブルバッファリングとは表示されている描画領域(表示領域)に直接描画処理を行うのではなく、表示領域と同じサイズのバッファを用意してそこで描画処理を行う手法です。バッファとはピクセルの情報を保存するメモリ空間のことを指します。一般的には表示領域をフロントバッファ、描画処理を行う領域をバックバッファと呼んでいます。ゲームループの処理の中で描画処理をバックバッファで行い、描画処理が終わったらバックバッファとフロントバッファと入れ替えてグラフィックを表示しています。

  • Back Buffer
    描画処理を行うためのバッファ
  • Front Buffer
    描画処理結果を表示する(レンダリング結果を保持する)バッファ

OpenGL では描画処理を行うバックバッファのことをフレームバッファ、描画処理結果を表示するフロントバッファのことをレンダーバッファといいます。またグラフィックが表示されている空間のことをサーフィス(Surface)と呼びます。図にすると以下のようになります。

ダブルバッファリングに関する詳しい仕組みや実装例なんかは以下の書籍(Game Programming Patterns)の第8章が参考になると思います。

フレームバッファ

描画処理を行うフレームバッファは役割により以下の3つのバッファに分かれています。

  • Color Buffer
    色情報を保持するバッファ
  • Depth Buffer
    別名深度バッファ、奥行き情報を保持するバッファ
  • Stencil Buffer
    物体の重ね合わせなどにより描画しなくてもよい領域を判定するための特殊なバッファ

図にすると以下のようになります。

OpenGL ES を使うための下準備(for iOS)

OpenGL ES のプログラムを書くための下準備をします。ここは対応 OS によって実装が変わるので適宜 OS ごとに実装を用意する必要があります。とりあえず iOS(Objective-C) で実装するのであればどうなるかを紹介します。iOS といっても GLKit を使わない前提でプログラムします。iOS の Open GL ES の下準備は以下の流れで行います(流れは他の OS でもだいたい同じになると思います)。

  • ゲームループの生成
  • コンテキストの生成
  • レンダーバッファのセットアップ
  • フレームバッファのセットアップ

プログラムは UIView クラスを継承したビューを作成してその中で行います。クラスの宣言部分は以下のようになります。

#import <UIKit/UIKit.h>
#import <OpenGLES/ES2/gl.h>

@interface GLSurfaceView : UIView

@end

では実際の OpenGL ES のセットアップ処理を実装部分に書いていきます。

#import "GLSurfaceView.h"

@implementation GLSurfaceView {
  // OpenGL コンテキストオブジェクト
  EAGLContext *_context;
}
// OpenGL ES 用のレイアーを使うことを明示する
+ (Class)layerClass {
  return [CAEAGLLayer class];
}
// ストーリーボードから呼ばれるイニシャライザ
- (instancetype)initWithCoder:(NSCoder*)coder
{
  if ((self = [super initWithCoder:coder])) {
    [self setUp];
    // ゲームループ60FPS
    [NSTimer scheduledTimerWithTimeInterval:1/60.0f target:self selector:@selector(update:) userInfo:nil repeats:YES];
  }
  return self;
}
// 各種セットアップ処理
- (void)setUp
{
  // レイヤーのセットアップ(iOS特有の処理)
  CAEAGLLayer *layer = (CAEAGLLayer*)self.layer;
  layer.opaque = YES;
  // コンテキストオブジェクトのセットアップ(iOS特有の処理)
  _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);
}
// ゲームループメソッド(1/60秒ごとに呼び出される)
- (void)update:(NSTimer *)timer
{
  // ここに描画処理を書く...
}

@end

OpenGL はコマンド単位で描画処理を行います。OpenGL はステートマシンで描画処理の状態を管理しています。コマンド実行後の状態はコンテキスト(Context)という名前のオブジェクトによって管理されます。コンテキストオブジェクトは各 OS ごとに用意する必要があるので、セットアップ方法は OS によって変わります。例えば iOS では以下のようになります。

// OpenGL ES 2.0用のコンテキストオブジェクトを生成する
[[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];

またコンテキストオブジェクトはスレッドごとに1つ生成する必要があります。

色を塗りつぶすプログラム

下準備がおわったので、画面全体を塗りつぶすプログラム書いてみましょう。描画処理はゲームループから呼び出されるメソッドの中に書いていきます。先ほどのセットアップ処理で作成した update メソッドに描画処理を実装します。塗りつぶす色を指定してカラーバッファに塗りつぶしを行います。プログラムにすると以下のようになります。

// ゲームループから呼び出される関数
- (void)update:(NSTimer *)timer
{
  // 色をセット
  glClearColor(0.0f, 1.0f, 1.0f, 1.0f);
  // カラーバッファを塗りつぶす
  glClear(GL_COLOR_BUFFER_BIT);
  // フレームバッファの内容を描画する(OS によって実装方法が異なります)
  [_context presentRenderbuffer:GL_RENDERBUFFER];
}

はじめに glClearColor 関数を呼んでコンテキストオブジェクトに色の情報を保存します。

次に glClear 関数を呼んでコンテキストオブジェクトに保存されている色情報を使って引数で指定されたバッファの色を塗りつぶします。

最後にフレームバッファの内容をレンダーバッファに渡して(バックバッファの内容をフロントバッファに置き換え)描画を行います。

コンテキストに設定されている色情報の取得

コンテキストオブジェクトに設定されている色情報を確認するには以下のように glGetFloatv 関数を使います。

GLfloat rgba[4];
glGetFloatv(GL_COLOR_CLEAR_VALUE, rgba);
NSLog(@"%f %f %f %f", rgba[0], rgba[1], rgba[2], rgba[3]);

実行結果

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

サンプルプログラム

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

次回は

第1回はここまでにします。最後までお付き合いいただきありがとうございました。次回は三角形を描画するプログラムを書いてみます。三角形を描くにはシェーダの設定が必要になるので、シェーダの基本的な説明ができればと考えています。