第4回 OpenGL ESを用いた簡単な図形の描画 実践編( 後半 )

カンデラの開発者による連載コラムです。 第4回は、「OpenGL ESを用いた簡単な図形の描画」の実践編(後半)です。

前回に引き続き、今回もOpenGL ESを用いた簡単な図形を描画するアプリケーションを作成します。前回までで、EGLの初期化と終了処理までを実装しました。今回は、シェーダ周辺の取り扱いと実際の描画処理を実装します。

目標の再確認

実践編の目標は、図1のような基本の三角形を描画できるようになることです。今回の実践編(後編)でもって、描画処理の実装も完了することになります。ただし、前回実装しましたプラットフォーム依存部についてはターゲットを絞り込んで具体的な実装をせず、空の関数のままにしていますので、動作するアプリケーションを作成する場合は、ターゲットに合わせた実装を別途行う必要があります。

図1. 今回の目標

構成

前回のアプリケーションの構成図をもう一度掲載しておきます(図2)。
 

図2. アプリケーションの流れ
 

今回は図3の青い四角の部分、描画処理の本体を実装することになります。それでは、それぞれについて見ていきましょう。
 

図3. 今回のターゲット

シェーダの準備

シェーダの準備としては大きく分けて、シェーダを記述すること、それを使えるようにすること( アプリケーション側からコンパイル/リンクして変数の受け渡しをできる状態にすること )、それらを解放することの3つの処理を実装することになります。

シェーダの記述

前回ご紹介したとおり、今回の描画では、図4にある2種類のシェーダ( 頂点シェーダ、フラグメントシェーダ )が必要になります。今回は文字列をOpenGL ESのドライバで直接コンパイルすることにしますので、まずはシェーダプログラムの文字列を用意します( リスト1 )。リスト1のシェーダでは、C言語に似たような文法で記述されていることがお分かりになると思います。
いずれのシェーダにも、attributeやvaryingという見慣れないキーワードがあります。これらはシェーダ内で取り扱うことのできる変数となります。uniform変数( 今回は未使用 )とattribute変数はアプリケーション側からシェーダに渡す変数です。uniform変数は、対象の図形を通して一貫性のある変数( 例えば頂点を一括して移動させる行列、サンプルしたいテクスチャなど )、attribute変数は頂点属性変数とも呼ばれていて、頂点毎に異なる変数( 例えば頂点の色など )を渡す際に使用します。今回は、頂点毎に場所と色を指定しますので、attribute変数に、頂点の座標と色を表すvector(vec2とvec4)を宣言しておきます。そして、シェーダ同士で値を受け渡す方法として、varying変数というものがあります。今回のケースですと、頂点シェーダからフラグメントシェーダへ、頂点色を渡したいので、色のvector( vec4 )を頂点シェーダとフラグメントシェーダの両方に宣言しておきます。
 

図4. シェーダとパイプラインステージ
 

// このソースファイルでしか使わないので無名名前空間に逃がす.
namespace {
    const GLchar* STR_VERTEX_SHADER = R"(
        attribute vec2 a_vPosition;
        attribute vec4 a_vColor;
 
        varying vec4 v_vColor;
 
        void main() {
            gl_Position = vec4( a_vPosition, 0.0, 1.0 );
            v_vColor = a_vColor;
        }
    )";
 
    const GLchar* STR_FRAGMENT_SHADER = R"(
        precision mediump float;
 
        varying vec4 v_vColor;
      
        void main() {
            gl_FragColor = v_vColor;
        }
    )";
}

リスト1. シェーダの文字列

シェーダを使えるようにする

上記で準備した、シェーダをコンパイル、リンクします。この操作が成功すると、描画で使用することのできるプログラムが完成します。そして、シェーダに変数を渡せる準備として、シェーダ上のAttribute変数を表す場所( AttribLocation )を取得しておきます。今回のシェーダにはuniform変数は存在しませんが、uniform変数を使用する場合も同様の場所( UniformLocation )を取得しておきます。これらのLocationは後に使いますので、何らかの変数などの保持しておきましょう。

これまでで準備したシェーダがアプリケーション内で不要になった場合、明示的に解放する必要があります。その方法は、シェーダを解放するというOpenGL ESのAPIをコールするだけです。

これらの処理をまとめると、リスト2のような内容になります。


// シェーダに関する設定を保持しておくための構造体.
typedef struct _SShader {
    GLuint unProgram;    // リンクしてできたシェーダプログラムのハンドル
 
    // シェーダ上の、座標と色のAttribute変数の場所を示す番号.
    GLint nAttrPos;
    GLint nAttrColor;
} SShader;
 
// -------- ここから前回にて宣言/定義済み --------
static GLuint CompileShader( GLenum eShaderType, const char* szSrc );
static bool InitializeShaders( SShader* pShader );
static void TerminateShaders( SShader* pShader );
// -------- ここまで前回にて宣言/定義済み --------
 
// 文字列を受け取りシェーダをコンパイルする関数.
static GLuint CompileShader( GLenum eShaderType, const char* szSrc )
{
    GLint nCompiled = 0;
    GLuint unShader = glCreateShader( eShaderType );
 
    glShaderSource( unShader, 1, &szSrc, nullptr );
    glCompileShader( unShader );
 
     // コンパイルの状態をチェック. 
    glGetShaderiv( unShader, GL_COMPILE_STATUS, &nCompiled );
    // 何らかのエラーがあり、エラー文字列が取得できる場合は表示.
    if( GL_FALSE == nCompiled ) {
        GLint nLength;
        char* msg = nullptr;
 
        glGetShaderiv( unShader, GL_INFO_LOG_LENGTH, &nLength );
      
        if( nLength ) {
            msg = reinterpret_cast<char*>( alloca( nLength * sizeof(char) ) );
            glGetShaderInfoLog( unShader, nLength, &nLength, msg );
            std::cerr << "Error glCompileShader." << std::endl;
            std::cout << msg << std::endl;
        }
        return 0;
    }
    return unShader;
}
 
static bool InitializeShaders( SShader* pShader )
{
    if( pShader ) {
        // リスト4のシェーダ文字列を渡して頂点シェーダとフラグメントシェーダをコンパイル.
        GLuint unVert = CompileShader( GL_VERTEX_SHADER, STR_VERTEX_SHADER );
        GLuint unFrag = CompileShader( GL_FRAGMENT_SHADER, STR_FRAGMENT_SHADER );
 
        // いずれもコンパイルができた場合、プログラム( 頂点/フラグメントシェーダをまとめたもの )を生成.
        if( unVert && unFrag ) {
            GLuint unProgram = glCreateProgram();
            GLint nLinkStatus = 0;
 
            glAttachShader( unProgram, unVert );
            glAttachShader( unProgram, unFrag );
            glLinkProgram( unProgram );
 
            // リンクの状態をチェック何らかのエラーがあり、エラー文字列が取得できる場合は表示. 
            glGetProgramiv( unProgram, GL_LINK_STATUS, &nLinkStatus );
            // 何らかのエラーがあり、エラー文字列が取得できる場合は表示.
            if( GL_FALSE == nLinkStatus ) {
                GLint nLength;
                char* msg = nullptr;
 
                glGetProgramiv( unProgram, GL_INFO_LOG_LENGTH, &nLength );
 
                if( nLength ) {
                    msg = reinterpret_cast<char*>( alloca( nLength * sizeof(char) ) );
                    glGetProgramInfoLog( unProgram, nLength, &nLength, msg );
                    std::cerr << "Error glLinkProgram." << std::endl;
                    std::cout << msg << std::endl;
                }
                return false;
            }
 
            // プログラムが無事生成されている場合シェーダ自身は不要であることをドライバに通知.
            glDeleteShader( unFrag );
            glDeleteShader( unVert );
 
            // 冒頭で定義した構造体にプログラムのハンドルと、Attributeの場所を保持しておく.
            pShader->unProgram = unProgram;
            pShader->nAttrPos = glGetAttribLocation( pShader->unProgram, "a_vPosition" );
            pShader->nAttrColor = glGetAttribLocation( pShader->unProgram, "a_vColor" );
        }
    }
    return true;
}
 
static void TerminateShaders( SShader* pShader )
{
    if( pShader ) {
        glDeleteProgram( pShader->unProgram );
        pShader->unProgram = 0;
        pShader->nAttrPos = -1;
        pShader->nAttrColor = -1;
    }
}

リスト2. シェーダを使えるようにする

描画する三角形の準備

シェーダが準備できたら、そのシェーダに渡す図形を準備します。今回は三角形を描画しますが、せっかくですので、頂点に色を付けたいと思います。座標系は前回の図(図5)を思い出してください。左下が( -1, -1 )、右上が( 1, 1 )となります。今回は画面の真ん中に三角形を描画しますので、座標的には図6のようにします。色は、上の頂点を赤( 1.0f, 0.0f, 0.0f, 1.0f )、左下の頂点を青( 0.0f, 1.0f, 0.0f, 1.0f )、右下の頂点を緑( 0.0f, 0.0f, 1.0f, 1.0f )にしましょう。頂点と色のそれぞれの情報を配列として宣言したものがリスト6の内容となります。今回は描画時にこの配列をGPUに毎フレーム転送しながら描画することにします。


図5. OpenGL ESの正規化デバイス座標系
 

図6. 描画する三角形の頂点
 

// このソースファイルでしか使わない変数群なので無名名前空間に逃がす.
namespace {
    const int32_t NUM_VERTICES = 3;                 // 三角形なので頂点の数は3つ.
    const int32_t NUM_COORD_ELEM_PER_VERTEX = 2;    // X座標とY座標の2つ.
 
    const int32_t NUM_COLOR_ELEM_PER_VERTEX = 4;    // 色はRGBAの4成分.
 
    const GLfloat VERTICES[NUM_VERTICES * NUM_COORD_ELEM_PER_VERTEX] = {
         0.0f,  0.5f,   // 上.
        -0.5f, -0.5f,   // 左.
         0.5f, -0.5f,   // 右.
    };
 
    const GLfloat COLORS[NUM_VERTICES * NUM_COLOR_ELEM_PER_VERTEX] = {
        1.0f, 0.0f, 0.0f, 1.0f,     // 上.
        0.0f, 1.0f, 0.0f, 1.0f,     // 左.
        0.0f, 0.0f, 1.0f, 1.0f,     // 右.
    };
}

リスト3. 頂点と色の配列の宣言

描画ループの準備

描画ループ自身は実は前回実装済みです。whileで記述した部分がこれに相当します。ここでは、そのループ内の処理を実装してくことになります。OpenGL ESでは前フレームの描画結果を一度クリアし、現在のフレームの内容を描き込むという手順を踏むのが一般的です。ですので、描画ループの頭の部分では、まず描画面のクリアを行います。次に、シェーダを有効化し、使用する頂点データをシェーダに伝えます。そして、描画命令を発行し、描画面を入れ替えます。描画面の入れ替えはあまり聞き慣れない言葉かもしれません。OpenGL ESの描画面は、裏面と表面の2枚が存在します。基本的には裏面に描画し、全ての描画が終了したら、裏面と表面を入れ替えます。このすべての描画が終了したら裏面と表面を入れ替えるというコマンドを実行するのが、描画面の入れ替え処理となります。また、プラットフォーム固有の何らかの処理を毎フレーム呼ぶ必要がある場合はその内容も記述することになります。それを想定して、前回用意したプラットフォーム依存部の空関数、PollNativeSystemが描画ループからコールされています。ここまでの内容をまとめると、リスト4のようになります。


// このソースファイル内でしか使わない定数は無名名前空間に逃がす.
namespace {
    // リスト3の頂点の情報に関してもここに定義されていると想定.
    // ( 略 ).
    // リスト3参照.
 
    const int32_t NUM_FRAMES = 600;                 // 指定回ループが回ったらこのプログラムを抜ける.
}
 
// -------- ここから前回にて宣言/定義済み --------
static void Draw( SShader* pShader, EGLDisplay& display, EGLSurface& surface );
static void DrawTriangle( SShader* pShader );
// -------- ここまで前回にて宣言/定義済み --------
 
static void Draw( SShader* pShader, EGLDisplay& display, EGLSurface& surface )
{
    // 描画面を指定色でクリアする.
    glClearColor( 0.25f, 0.25f, 0.5f, 0.0f );
    glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
 
    // 三角形の描画処理本体.
    DrawTriangle( pShader );
 
    // 裏面と表面を入れ替える.
    eglSwapBuffers( display, surface );
}
 
static void DrawTriangle( SShader* pShader )
{
    // ここで、リスト2のシェーダを使う.
    if( pShader ) {
        // シェーダプログラムを使うと宣言.
        glUseProgram( pShader->unProgram );
 
        // 頂点属性配列を有効化.
        glEnableVertexAttribArray( pShader->nAttrPos );
        glEnableVertexAttribArray( pShader->nAttrColor );
 
        // まずは座標を転送.
        // 第5引数はNUM_COORD_ELEM_PER_VERTEX * sizeof(GLfloat)でも可.
        // 今回はデータが隙間なく埋まっているので0でも良い.
        glVertexAttribPointer(
            pShader->nAttrPos,
            NUM_COORD_ELEM_PER_VERTEX,
            GL_FLOAT,
            GL_FALSE,
            0, // NUM_COORD_ELEM_PER_VERTEX * sizeof(GLfloat),
            VERTICES
        );
 
        // 次に色を転送.
        // 第5引数NUM_COLOR_ELEM_PER_VERTEX * sizeof(GLfloat)でも可.
        // 今回はデータが隙間なく埋まっているので0でも良い.
        glVertexAttribPointer(
            pShader->nAttrColor,
            NUM_COLOR_ELEM_PER_VERTEX,
            GL_FLOAT,
            GL_FALSE,
            0, // NUM_COLOR_ELEM_PER_VERTEX * sizeof(GLfloat),
            COLORS
        );
 
        // 上記設定で描画.
        glDrawArrays( GL_TRIANGLES, 0, NUM_VERTICES );
 
        // この頂点属性配列は以降は使用しないので、無効化.
        glDisableVertexAttribArray( pShader->nAttrColor );
        glDisableVertexAttribArray( pShader->nAttrPos );
 
        // 描画プログラムも使用しない.
        glUseProgram( 0 );
    }
}
 
// 前回定義したmain関数( 一部省略 ).
int main( int argc, char *argv[] )
{
    // 変数宣言など省略.
    ...
 
    // 略.
    if( ... ) {
        if( ... ) {
            if( ... ) {
                // 指定回ループが回ったらアプリケーションを抜ける.
                GLint nFrameCount = 0;
                while( nFrameCount < NUM_FRAMES ) {
                    // Nativeの何らかの定例処理.
                    PollNativeSystem( &display, &window );
 
                    // 描画処理.
                    Draw( &sShader, eglDisplay, eglSurface );
                    ++nFrameCount;
                }
                ...
            }
            ...
        }
        ...
    }
 
    return 0;
 }

リスト4. 頂点と色の配列の宣言

前回と今回ご紹介した内容を統合し、プラットフォーム依存部を実装すると、特定のターゲットで動作するアプリケーションが完成します。描画結果として図1のような三角形が表示されるはずです。ここまでOpenGL ESを用いて、三角形を一つ描画するために、長い手順を踏んだことになります。三角形以外の別の図形を描画したい場合は、新たにシェーダを用意して、描画したい図形の頂点データを用意し、描画ループでそのシェーダを有効にして、頂点データを渡し描画するという流れになります。

 

OpenGL ESを用いた基本的な図形の描画に関するご紹介は以上となりますが、3D空間に物体を描画する場合は、モデルデータ、ライト、カメラ、テクスチャなど、更に複雑な要素が絡んできます。また、より実用的なアプリケーションを目指す場合は、効率よく描画するための手法なども検討して実装していく必要があります。

 

次回は、2Dのグラフィックに特化している描画API、OpenVGを用いて基本的な図形を描画してみたいと思います。