ホーム < ゲームつくろー! < OpenGL ES 2.0技術編

その2 エミュレータ上でHello Triangle


 前章でOpenGL ES 2.0をWindows上でエミュレートするのに必要なlibとdll(ANGLE)を作成しました。この章では早速それらを用いてポリゴン三角形を描画してみましょう。



@ プロジェクトを立ち上げる

 空っぽの状態から行きましょう。まずVisualStudioを立ち上げてWin32プロジェクトを作りましょう。名前はもちろん「HelloTriangle」です。次にmain.cppを追加します。ここにサンプルプログラムをざーっと書いていきます。

 main.cppで最低限ビルド出来るコードは次の通りです:

#include <tchar.h>
#include <Windows.h>

int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) {
    return 0;
}

実行すると一瞬だけ立ち上がって終わるだけのアプリになります。

 さて、ここからOpenGL ES 2.0なプログラムを組んでいくわけですが、OpenGLは各プラットフォームの差異を完全に吸収しているわけではありません。どう考えてもWindowsとMacとLinuxとでウィンドウの作り方は違う訳です。それをフルスクラッチで書くのはちょっと大変です。そこでANGLEの中に含まれている「ESフレームワーク」を利用する事にします。

 ESフレームワークは「OpenGL ES 2.0プログラミングガイド」という書籍がgooglecodeで公開しているOpenGL ES 2.0のフレームワークで、以下のサイトに指定されているSVNリポジトリをチェックアウトすると取得できます:

https://code.google.com/p/opengles-book-samples/source/checkout

 ここのCommonというフォルダ内にESフレームワークがあります。全く同じ物がANGLEのsample/gles2_book/Common下にありますので、ありがたく使用させて頂く事にしましょう。


○ 再頒布について

 ANGLEはOSSですが、ANGLEのフレームワークで出来たlibEGL.lib、libEGL.dll、libGLESv2.lib、libGLESv2.dll等のバイナリファイルを頒布するにはANGLEフォルダ下にあるLICENSEファイルを一緒に添付する必要があります。詳しくはそのLICENSEファイルをご覧ください。
(ESフレームワークについてはライセンスが含まれていないようなので、使用して構わないと思います)



A ウィンドウを表示するだけプログラム

 ではESフレームワークを使ってウィンドウを表示するだけのプログラムを作ってみます。Windowsエミュレート用のESフレームワークは、ANGLE/samples/gles2_book/Common以下にある、

・ esUtil.h
・ esUtil.c
・ esUtil_win.h
・ Win32/esUtil_win32.c
・ Win32/esUtil_TGA.c

という5つのコードと、先の章で作成した、

・ libEGL.lib
・ libGLESv2.lib

の2つのlibファイル、そして実行に必要な、

・ libEGL.dll
・ libGLESv2.dll

の2つのdllファイルがあると動きます。一先ずこれらをプロジェクト直下にコピーしてきましょう:

次にdll以外のコードとlibをプロジェクトに追加します。これで準備が整いました。

 ESフレームワークでは、「ESContext」というコンテキスト構造体を通してあらゆる事を行います。ESContext構造体はesUtil.hで定義されていて、色々メンバが入っているのですが、今はあまり気にせずに行きましょう。

 ESフレームワークを用いると、ウィンドウを表示するだけのプログラムはとても簡単に出来てしまいます:

ESフレームワークでウィンドウを表示する(main.cpp)
#include <tchar.h>
#include <Windows.h>
#include "esUtil.h"

int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) {

    // コンテキスト作成
    ESContext esContext;
    esInitContext( &esContext );

    // ウィンドウ作成
    esCreateWindow( &esContext, TEXT("Hello Triangle"), 320, 240, ES_WINDOW_RGB );

    // ループ開始
    esMainLoop( &esContext );

    return 0;
}

 コンテキストをメイン関数内のローカル領域に作成するのがポイントです。これは環境によってはグローバル領域が殆ど使えない事があるからだそうです。

 コンテキストはesInitContext関数を通すと初期化されます。初期化済みのコンテキストをesCreateWindow関数に渡すとプラットフォームに合わせたウィンドウを作ってくれます。引数は言わずもがなかもしれません。最後のES_WINDOW_RGBはRGBカラーなウィンドウを作ってねというフラグです。

 ウィンドウを作成したらesMainLoop関数にコンテキストを渡すとゲームループが開始されます。

 このメイン関数内にWindows専用な香りが無いのが大切で、関数内のコードを別の.cpp内に記述すれば他のプラットフォームでも使い回しができるようになるわけです。

 これで実行すると320×240の真っ黒バックなウィンドウが表示されます:

キャプションがesCreateWindowに渡した名前になっていますね。

 で、ここからが本番となります。



B UserDataを定義

 コンテキスト(ESContext)構造体には「userData」というvoid*型のメンバが一つあります。これはその名の通りユーザが使いたい任意のデータを格納するメモリです。これは本当に任意なので作るゲームやアプリケーションで都度定義します。

 今回はここに「programObjectId」というメンバを持ったUserData構造体を渡す事にしましょう。まずUserData構造体を定義します:

UserData構造体
struct UserData {
    GLuint programObject;
};

 GLuintというのはgl2.hで定義されているunsigned int型のtypedefです。「unsigned int」と書きたくなる所ですが、OpenGLを使う時のお作法として上のようにtypedefされた型名を使う事を推奨します。これは他のプラットフォームの環境で型のサイズや意味を合わせる為にも重要です。

 将来的にユーザデータとして使いたい物が増えたら、この構造体に適宜メンバが追加されていく事になります。

 メイン関数内にUserDataオブジェクトを一つ作ってコンテキストに渡せば使い回せるようになります:

int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) {

    // コンテキスト作成
    ESContext esContext;
    esInitContext( &esContext );

    // ウィンドウ作成
    esCreateWindow( &esContext, TEXT("Hello Triangle"), 320, 240, ES_WINDOW_RGB );

    // ユーザデータ作成
    UserData userData;
    esContext.userData = &userData;

    // ループ開始
    esMainLoop( &esContext );

    return 0;
}



C 描画関数の登録とカラーバッファのクリア

 OpenGL ES 2.0では内部にゲームループを持っていて、毎フレーム描画関数が呼ばれる仕組みが整っています。ただし、描画関数は登録してあげないと呼んでくれません。

 描画関数は型が指定されていて、次のような宣言になります:

描画関数
void draw( ESContext *esContext );

引数にコンテキストへのポインタが渡されます。この描画関数の関数ポインタをesRegisterDrawFunc関数に渡すと登録となります:

void draw( ESContext* esContext ) {
   ...
}

int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) {

    // コンテキスト作成
    ESContext esContext;
    esInitContext( &esContext );

    // ウィンドウ作成
    esCreateWindow( &esContext, TEXT("Hello Triangle"), 320, 240, ES_WINDOW_RGB );

    // ユーザデータ作成
    UserData userData;
    esContext.userData = &userData;

    // 描画関数を登録
    esRegisterDrawFunc( &esContext, draw );

    // ループ開始
    esMainLoop( &esContext );

    return 0;
}

 描画関数の中では毎フレーム描画する内容を全部記述する事になります。今は何も世界に無い状態なので、とりあえずウィンドウの背景(バックバッファ)をクリアする部分だけを書いてみます:

draw関数
void draw( ESContext *esContext ) {
    // クリアカラーを指定
    glClearColor( 1.0f, 1.0f, 0.0f, 0.0f );

    // バックバッファをクリア
    glClear( GL_COLOR_BUFFER_BIT );

    // バックバッファをフロントにフリップ
    eglSwapBuffers( esContext->eglDisplay, esContext->eglSurface );
}

 glClearColor関数でクリアしたい色をRGBAで指定します。上では黄色にしています。次にg;Clear関数の引数にGL_COLOR_BUFFER_BITフラグを渡しglClearColor関数で指定した色でバックバッファをクリアするよう命令します。本番ではこの後にあらゆる描画処理が行われ、バックバッファに描き込まれていきます。で、最後にeglSwapBuffers関数でバックバッファとフロントバッファがフリップ(スワップ)され、画面にお目見えする事になります。

 関数の接頭子に「gl」とあるのはOpenGLが定義している関数です。一方「egl」というのはEGLというOpenGLと各プラットフォームの仲立ちをして描画周りの差異を吸収してくれるライブラリの関数です。EGLがあるお陰で、Windows内でエミュレート描画した物がAndroidや他のプラットフォームでも同じように描画されるわけです。

 上の段階の実装で、黄色で塗りつぶされたウィンドウが表示されます:

 さて、ではポリゴンを描画するにはどうするか?そのためにはポリゴンその物を表す「頂点情報」だけでなく「ポリゴンをどう描画するか」という「シェーダ」が必要になります。OpenGL ES 2.0には固定機能が無いため、描画方法はプログラマがシェーダを通して記述しなければならないんです。



D 頂点シェーダとフラグメントシェーダの書き方

 OpenGL ES 2.0から描画方法はシェーダを使うのみとなりました。今後固定機能が復活する事は多分無いので、こういうもんだと思って取り組んでしまいましょう。

 OpenGL ES 2.0のシェーダは「頂点シェーダ」と「フラグメントシェーダ」の2つです。頂点シェーダはポリゴンのスクリーン内での位置を決めるプログラムで、フラグメントシェーダはそのポリゴンの表面をどう塗るかを決めるシェーダです。

 ポリゴンは三角形で、3つ頂点の座標で形が決まります。頂点シェーダの引数にはこのポリゴンの頂点座標が一つずつ入ってきます。頂点シェーダの出力はスクリーン座標上の頂点の位置です(正しくはスクリーンを(-1,-1)〜(1,1)という矩形とみなした時の座標)。

 頂点シェーダで決められたポリゴンの描画位置から、そのポリゴンを塗りつぶす為のピクセル位置が決まります(ラスタライズ)。フラグメントシェーダにはそのピクセルが一つずつやってくるので、それを何色に塗るかプログラムで指定して出力します。

 図でイメージするとこんな感じでしょうか:

 今回のHello Triangleサンプルは、頂点シェーダに入って来る三角形の座標がすでにスクリーン座標になっているとして、頂点シェーダはその値をそのまま出力します。フラグメントシェーダではポリゴンを占めるピクセル全部を赤色に塗るとします。

 OpenGL ES 2.0の頂点シェーダはこんな感じです:

頂点シェーダ
attribute vec4 vPosition;

void main() {
   gl_Position = vPosition;
}

こういうコードをテキストで用意します。ちなみにC言語ライクですが、C言語上で動くプログラムという訳では無いので注意です。

 まず「attribute」というのは頂点シェーダの引数を表します。続くvec4は4成分のベクトルで、ここでは頂点の座標を受け取るために設けています。変数名のvPositionというのは別に決まっている訳ではないので何でも構いません。頂点シェーダの本体はmain関数内で、上の例にある「gl_Position」という変数に渡した座標が出力座標として扱われます。これは絶対に渡さなければならない値です。出力変数名が厳密に決まっているのにも注意です。

 OpenGL ES 2.0のフラグメントシェーダはこんなです:

フラグメントシェーダ
precision mediump float;

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

 precisionというのはデフォルトの計算精度を指定する宣言で、上では後に続くmediumpという精度修飾子によって「floatは中間程度の精度で計算してね」と指定しています。これはOpenGLには無いOpenGL ES 2.0独自の仕様です。

 なぜこれが必要かというと、OpenGL ES 2.0は低スペックで電力量に乏しい携帯端末等をターゲットにしているためです。計算精度を落とす事により高速に動作したり電力を食わなくなります。これにより携帯端末でもサクサクとゲームが動くようになるわけです。精度修飾子にはlowp、mediump、highpの3種類があります。

 フラグメントシェーダの本体もmain関数内にあります。上の場合、「gl_FragColor」という出力用の変数に赤色を指定しています。これにより、頂点シェーダで計算されたポリゴンの領域を構成するすべてのピクセルが赤色になる訳です。

 シェーダについては後章でいっぱい出てくると思いますので、ここではこうして作った頂点シェーダとフラグメントシェーダを使う方法に的を絞ります。



E シェーダの使い方〜コンパイル

 Dで記述したシェーダは単なる文字列のプログラムです。これをOpenGL ES 2.0で使うには、このプログラムをコンパイルする必要があります。そのコンパイル作業はOpenGL ES 2.0のプログラム内で行います。

 まず、Dのシェーダプログラムをそれぞれ文字列としてメモリに格納しましょう:

シェーダプログラム
GLbyte vShaderStr[] =
    "attribute vec4 vPosition;    \n"
    "void main() {                \n"
    "    gl_Position = vPosition; \n"
    "}                            \n";

GLbyte fShaderStr[] =
    "precision mediump float;   \n"
    "void main() {              \n"
    "    gl_FragColor = vec4 ( 1.0, 0.0, 0.0, 1.0 );\n"
    "}                          \n";

 上のようにプログラム上に直接コード文字列を書いても良いのですが、普通は外部のテキストファイルを読み込んで格納します。メモリに格納すればどちらも同じです。

 この文字列なシェーダをコンパイルするまでには次のような道筋を辿ります:

 シェーダオブジェクトというのは頂点シェーダ及びフラグメントシェーダを管理するオブジェクトで、頂点シェーダとフラグメントシェーダそれぞれ専門のオブジェクトが必要になります。どちらのシェーダオブジェクトを作るかはglCreateShader関数の引数で指定します:

glCreateShader関数
GLuint vShaderObject = glCreateShader( GL_VERTEX_SHADER );
GLuint fShaderObject = glCreateShader( GL_FRAGMENT_SHADER );

 オブジェクトと言っていますが、戻り値は整数値なのに注目です。OpenGLはC言語ベースなので、使用者側はオブジェクトに触る事が出来ません。変わりに整数値(ID)を貰いそれをキーにしてOpenGLの内部に保持されているオブジェクトに要求を出します。

 各シェーダのシェーダオブジェクトを作ったら、glShaderSource関数を通してそこにシェーダコードを登録します:

glShaderSource関数
glShaderSource( vShaderObject, 1, &vShaderStr, 0 );
glShaderSource( fShaderObject, 1, &fShaderStr, 0 );

 第1引数にはシェーダオブジェクトのIDを渡します。
 第2引数にはシェーダコードの数を指定します。つまりこの関数で複数のシェーダコードを一つのシェーダオブジェクトに登録する事ができるんです。
 第3引数にはシェーダコード文字列の配列を渡します。これ「文字列の配列」なのでダブルポインタ(const GLchar**)になっています。
 最後の第4引数は各文字列の長さです。これがNULL(0)の場合、第3引数の文字列はnull端末だと仮定されます。

 これでシェーダリソースにシェーダコードが登録されました。続いてそれをglCompileShader関数でコンパイルします:

glCompileShader関数
glCompileShader( vShaderObject );
glCompileShader( fShaderObject );

 シェーダオブジェクトIDを渡すだけです。この関数、特に戻り値もありません。一応これでシェーダオブジェクト内にコンパイルされたシェーダが格納されます…が、コードが正しいかどうかこれではわかりません。

 シェーダが正しくコンパイルされたかを知るにはglGetShaderiv関数を使います:

シェーダのコンパイル状態をチェック
GLint compiled;
glGetShaderiv( vShaderObject, GL_COMPILE_STATUS, &compiled );

 この関数の第1引数にシェーダオブジェクトID、第2引数にGL_COMPILE_STATUS(コンパイル状態を要求)を渡すと、第3引数にコンパイルの状態がGL_TRUEかGL_FALSEで帰ってきます。GL_TRUEならコンパイルに成功しているのでシェーダを使う事ができます。

 GL_FALSEでコンパイルに失敗している事が分かった場合、どういうコンパイルエラーなのかを文字列で取得する事ができます:

シェーダのコンパイルエラーを知る
if ( compiled == GL_FALSE ) {
    // コンパイルエラーの文字列の長さを取得
    GLint logLen;
    glGetShaderiv( vShaderObject, GL_INFO_LOG_LENGTH, &logLen );

    // コンパイルエラー文字列を取得
    char *log = new char[ logLen + 1 ];
    glGetShaderInfoLog( vShaderObject, logLen, 0, log );
   
    delete[] log;
}

glGetShaderiv関数の第2引数にGL_INFO_LOG_LENGTHを渡すとエラー文字列の長さが第3引数に帰ります。その分だけの文字列を格納するメモリを確保し、glGetShaderInfoLog関数で実際に文字列を格納してもらいます。上の例ではlog内にその文字列が返ります。実際にちょっと間違った頂点シェーダをコンパイルすると、こんな文字列が返ってきました:

ERROR: 0:4: 'vPos' : undeclared identifier
ERROR: 0:4: 'assign' : cannot convert from 'float' to 'Position highp 4-component vector of float'

シェーダ内のvPositionをvPosと書き換えた場合のエラーです。なるほど、わかりますね。

 こういう感じに頂点シェーダとフラグメントシェーダが正しくコンパイルできれば、そのシェーダをコード内で使う事ができます。



F シェーダの使い方〜プログラムオブジェクト

 コンパイルに成功したシェーダは、そのままだと「頂点シェーダ」「フラグメントシェーダ」というプログラム断片です。頂点シェーダとフラグメントシェーダは繋がらないと機能しません。また頂点シェーダには頂点情報を流す必要がありますが、その入口となる「vPosition」にもアクセスできるようにしなければなりません。そういう繋ぎを管理し入口を提供するのが「プログラムオブジェクト」です:

上の図のような塊を作っていきます。

 まず空のプログラムオブジェクトを作り、頂点シェーダとフラグメントシェーダオブジェクトをくっつけます:

プログラムオブジェクトにシェーダをアタッチ
GLuint programObject;
programObject = glCreateProgram();

glAttachShader( programObject, vShaderObject );
glAttachShader( programObject, fShaderObject );

 空のプログラムオブジェクトはglCreateProgram関数で作ります。プログラムオブジェクトにシェーダオブジェクトをくっつけるのがglAttachShader関数です。第1引数に管理者たるプログラムオブジェクト、第2引数にシェーダオブジェクトを渡します。

 頂点シェーダが持っている入力用変数へアクセスを確保するにはglBindAttribLocation関数を使います:

頂点シェーダ入力変数へのアクセスを確保
glBindAttribLocation( programObject, 0, "vPosition" );

 第1引数にプログラムオブジェクト、第2引数にはアクセスする為のIDを振ります。これは特定のIDなら良いようです。第3引数に頂点シェーダで宣言したattributeな変数の名前を指定します。上の場合、vPositionがID=0としてプログラムオブジェクト内で認識されます。

 これでシェーダをくっつけて頂点シェーダの入力変数へのアクセスも確保しました。最後に、これらを全部接続(Link)します:

プログラムオブジェクトを完成させる
glLinkProgram( programObject );

うまくいっていれば、内部で各シェーダがリンクし、プログラムオブジェクトは一つの描画機能を提供するプログラムの塊となります。うまく言っているかどうかはglGetProgramiv関数でチェックします:

プログラムオブジェクトができたかチェック
GLint linked;
glGetProgramiv( programObject, GL_LINK_STATUS, &linked );

glGetProgramiv関数の第引数にGL_LINK_STATUSを渡すと第3引数に結果が返ってきます。もし正しく出来ていればGL_TRUE、失敗しているとGL_FALSEが返ります。

 失敗していた場合はシェーダコンパイル時と同様に理由をエラー文字列として取得する事ができます:

プログラムオブジェクトのリンクエラーを取得
if ( linked == GL_FALSE ) {
    GLint infoLen = 0;
    glGetProgramiv( programObject, GL_INFO_LOG_LENGTH, &infoLen );

    char* infoLog = new char[ infoLen ];
    glGetProgramInfoLog( programObject, infoLen, NULL, infoLog );

    delete[] infoLog;
}

glGetProgramiv関数でエラー文字列の長さを得て、glGetProgramInfoLog関数でエラー文字列を格納します。

 プログラムオブジェクトがうまく出来れば、ようやく描画処理でそれを使う事ができます。BのUserData構造体にprogramObjectというメンバがあったのを覚えていますでしょうか?完成したプログラムオブジェクトのIDをここに格納しておきます:

プログラムオブジェクトIDを保存
esContext->userData.programObject = programObject;

さぁもう少し、頂点情報を作って描画するまでのプロセスです。



G 頂点情報の作成と描画

 描画対象である三角ポリゴンは3頂点で構成されます。OpenGL ES 2.0ではこの情報を単純な浮動小数点の配列で扱います。今回はスクリーン座標を直接指定します:

頂点座標を定義
GLfloat vVertices[] = {
    0.0f,  0.5f, 0.0f,
  -0.5f, -0.5f, 0.0f,
    0.5f, -0.5f, 0.0f
};

スクリーン座標は画面の縦横がどんな比率であっても、(-1, -1)〜(1, 1)という正方形な範囲として頂点シェーダから出力されます。上の三角形はこんな絵を想定しているわけです:

この小さな正方形スクリーンをウィンドウの大きさに拡大するのがビューポート設定で、それを設定するのがglViewport関数です:

頂点座標を定義
glViewport( 0, 0, esContext->width, esContext->height );

第1、2引数はスクリーンの左上の座標で、よほどの事が無い限りは(0, 0)です。第3、4引数はスクリーンの幅と高さです。これはコンテキストの中に保存されているのでそれを活用します。ビューポートを設定する事で、最終的な画面出力サイズが確定します。

 先程作成した頂点情報をプログラムオブジェクトに流す作業に移ります。まず、プログラムオブジェクトを使う宣言をします:

プログラムオブジェクトの仕様宣言
glUseProgram( userData->programObject );

 glUseProgram関数でその宣言を行います。引数にプログラムオブジェクトのIDを指定します。これで内部にシェーダがセットされます。

 頂点情報をOpenGL内にセットするにはglVertexAttribPointer関数を使います。この関数はちょっと複雑なので定義を示します:

glVertexAttribPointer関数
void glVertexAttribPointer(
    GLuint indx,
    GLint size,
    GLenum type,
    GLboolean normalized,
    GLsizei stride,
    const GLvoid* ptr
);

 indxはプログラムオブジェクトの中で認識済みの頂点シェーダ入力変数のIDです。FでvPositionに対応したIDを定義しましたが、あれです。
 sizeは1頂点のindxのサイズです。今頂点座標は(x,y,z)のfloat3つ分なので、ここには「3」が入ります。
 typeはここに入れる頂点情報の型を指定します。頂点座標はfloatなのでGL_FLOATとなります。これは各型を表すマクロが用意されています。
 normalizedは頂点情報が正規化されているかを表します。頂点座標は正規化しない情報なのでGL_FALSEを入れます。法線などはGL_TRUEが入る事もあるかなと思います。
 strideは次の頂点情報までのオフセットサイズです。まじめに12 (sizeof(float) * 3)を入れても良いのですが、0を入れるとsizeとtypeからオフセットサイズを計算してくれます。
 ptrに頂点情報の配列を渡します。

 先の頂点情報の場合はこうなります:

glVertexAttribPointer関数を使用
glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, 0, vVertices );

 ID=0(vPosition)に対して頂点情報を設定、サイズはfloat×3で、正規化はされていない、次の情報までは12バイト(自動計算)です、っと設定しています。

 こうしてID=0に頂点座標をセットしたのですが、実はこの段階だとセットした情報がシェーダに流れません。これを流れるようにするには「ID=0を開いて下さい〜」と指定する必要があります:

頂点属性(attribute)の使用
glEnableVertexAttribArray( 0 );

glEnableVertexAttribArray関数がその役を担ってくれます。

 さ、これで後は「描画して下さい!」っと言うと描画プロセスが走ります。それをするのがglDrawArrays関数です:

描画指示
glDrawArrays( GL_TRIANGLES, 0, 3 );

 第1引数にはポリゴンの型を指定します。今回のポリゴンは三角形ポリゴン1枚なのでバラバラの三角形を意味するGL_TRIANGLESを指定します。
 第2引数はglVertexAttribPointer関数で指定した配列の何番目から描画を始めるかを指示します。大抵は0になりますが、例えば複数のモデルを一つの配列にまとめている場合などはこれで先頭位置を指定する事になります。
 第3引数は何頂点描画するかです。

 これにより、頂点座標が頂点シェーダの入力変数であるvPositionに流れ、スクリーン上のポリゴン位置が決まり、さらにフラグメントシェーダによってそのポリゴン領域に色が塗られます。その色はバックバッファに穿たれる訳です。

 そのバックバッファをフロントにフリップするのが最後の作業です:

バックバッファフリップ
eglSwapBuffers( esContext->eglDisplay, esContext->eglSurface );

ここだけはOpenGLの関数では無くてEGLライブラリの関数が使われます。これはプラットフォームの差異を吸収する必要があるためです。

 頂点情報は一度作成すれば良いのですが、それをOpenGLに設定して、プログラムオブジェクトを指定し描画処理をする作業は毎フレーム行う必要があります。つまり毎フレーム呼び出されるdraw関数内でそれをするわけです。



H サンプル描画結果

 以上を踏まえてHello Triangleのサンプルを作って動かすと、

と赤い三角形ポリゴンが描画されました!


 一通り説明しているので長い章でしたが、サンプル自体は実はとても短くなります。シェーダのエラーチェックを省いてmain.cppをぎゅっと詰めて書いたのが以下のコードです:

main.cpp
#include <tchar.h>
#include <Windows.h>
#include "esUtil.h"


struct UserData {
    GLuint programObject;
};


GLuint loadShader ( GLenum type, const char *shaderSrc ) {

    GLuint shader;

    shader = glCreateShader ( type );
    glShaderSource ( shader, 1, &shaderSrc, NULL );
    glCompileShader ( shader );

    GLint compiled;
    glGetShaderiv ( shader, GL_COMPILE_STATUS, &compiled );

    return shader;
}


int init ( ESContext *esContext ) {

    UserData *userData = (UserData*)esContext->userData;

    GLbyte vShaderStr[] =
        "attribute vec4 vPosition; \n"
        "void main() \n"
        "{ \n"
        " gl_Position = vPosition; \n"
        "} \n";

    GLbyte fShaderStr[] =
        "precision mediump float;\n"
        "void main() \n"
        "{ \n"
        " gl_FragColor = vec4 ( 1.0, 0.0, 0.0, 1.0 );\n"
        "} \n";

    GLuint vertexShader;
    GLuint fragmentShader;

    vertexShader = loadShader( GL_VERTEX_SHADER, (const char*)vShaderStr );
    fragmentShader = loadShader( GL_FRAGMENT_SHADER, (const char*)fShaderStr );

    GLuint programObject;
    programObject = glCreateProgram();

    glAttachShader ( programObject, vertexShader );
    glAttachShader ( programObject, fragmentShader );

    glBindAttribLocation ( programObject, 0, "vPosition" );
    glLinkProgram ( programObject );
    userData->programObject = programObject;

    return TRUE;
}


void draw( ESContext *esContext ) {

    UserData *userData = (UserData*)esContext->userData;

    GLfloat vVertices[] = {
         0.0f, 0.5f, 0.0f,
        -0.5f, -0.5f, 0.0f,
         0.5f, -0.5f, 0.0f
    };

    glViewport ( 0, 0, esContext->width, esContext->height );

    glClearColor ( 1.0f, 1.0f, 0.0f, 0.0f );
    glClear ( GL_COLOR_BUFFER_BIT );

    glUseProgram ( userData->programObject );

    glVertexAttribPointer ( 0, 3, GL_FLOAT, GL_FALSE, 0, vVertices );
    glEnableVertexAttribArray ( 0 );
    glDrawArrays ( GL_TRIANGLES, 0, 3 );

    eglSwapBuffers ( esContext->eglDisplay, esContext->eglSurface );
}


int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) {

    ESContext esContext;
    esInitContext( &esContext );
    esCreateWindow( &esContext, TEXT("Hello Triangle"), 320, 240, ES_WINDOW_RGB );

    UserData userData;
    esContext.userData = &userData;

    if ( !init( &esContext ) )
        return 0;

    esRegisterDrawFunc( &esContext, draw );

    esMainLoop( &esContext );

    return 0;
}

これくらいのコード量で三角ポリゴンが描画できるのですから、ESフレームワークにありがたさを感じますね。

 この章のポリゴンはスクリーン座標を直接指定したものなので、3Dを厳密に使っていません。次の章ではOpenGL ES 2.0で3Dモデルを描画してみます。