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

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

前回の内容で、OpenGL ESを用いて何らかの図形を描画する際に必要な作業については想像ができたかと思います。
そこで、より具体的なイメージをつかむため、今回と次回の2回に渡り、OpenGL ESを使い実際に簡単な図形を描画してみたいと思います。

実践編の目標は?

実践編の最終的な目標としては、図1のような基本の三角形を描画できるようになることです。ただし、実践編はターゲットの想定などをせず、C/C++を用いてのOpenGL ESやEGLのAPI呼び出しに特化して説明していきます。このため、本編の内容をコピーしただけでは、そのまま動作するアプリケーションにはならないことは予めご了承ください。
 

図1. 今回の目標

構成

アプリケーションの構成としては図2の流れを想定とします。各種初期化・終了処理に加えて一定時間経過後にアプリケーションを終了する処理も実装することにします。
 

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

残念ながら、この実装を全て1回分の記事に収めるのは難しいため、今回は図3の青い四角の部分をターゲットとしたいと思います。それでは、それぞれについて見ていきましょう。
 

図3. 今回のターゲット

エントリポイント

まずはアプリケーションのエントリポイントを用意します。図2の内容を考慮しつつ、リスト1のように空の関数( または宣言のみ )を用意しておきます。まだ定義していない型や定数などはありますが、後に使いますので、今は図2の内容と大体の対応が取れていることを確認しましょう。

 

// プラットフォーム共通化のために、それらしい型を定義しておく.
// 64ビット環境で、DisplayやWindowがint32_tの場合などが考えられるので、環境に合わせて適宜変更する.
typedef void* AppNativeDisplay;
typedef void* AppNativeWindow;
 
// プラットフォーム依存部.
static bool InitializeNativeSystem( AppNativeDisplay* pNativeDisplay, AppNativeWindow* pNativeWindow );
static void PollNativeSystem( AppNativeDisplay* pNativeDisplay, AppNativeWindow* pNativeWindow );
static void TerminateNativeSystem( AppNativeDisplay* pNativeDisplay, AppNativeWindow* pNativeWindow );
 
// EGL関連.
static bool InitializeEGL( 
    AppNativeDisplay* pNativeDisplay, 
    AppNativeWindow* pNativeWindow, 
    EGLDisplay& display, EGLContext& context, EGLSurface& surface );
static void TerminateEGL( EGLDisplay& display, EGLContext& context, EGLSurface& surface );
 
// シェーダ関連.
static GLuint CompileShader( GLenum eShaderType, const char* szSrc );
static bool InitializeShaders( SShader* pShader );
static void TerminateShaders( SShader* pShader );
 
// 描画ループ関連.
static void Draw( SShader* pShader, EGLDisplay& display, EGLSurface& surface );
static void DrawTriangle( SShader* pShader );
 
int main( int argc, char *argv[] )
{
    AppNativeDisplay display = nullptr;
    AppNativeWindow window = nullptr;
 
    EGLDisplay eglDisplay = EGL_NO_DISPLAY;
    EGLContext eglContext = EGL_NO_CONTEXT;
    EGLSurface eglSurface = EGL_NO_SURFACE;
 
    SShader sShader;
 
    if( InitializeNativeSystem( &display, &window ) ) {
        if( InitializeEGL( &display, &window, eglDisplay, eglContext, eglSurface ) ) {
            if( InitializeShaders( &sShader ) ) {
                GLint nFrameCount = 0;
                while( nFrameCount < NUM_FRAMES ) {
                    PollNativeSystem( &display, &window );
 
                    Draw( &sShader, eglDisplay, eglSurface );
                    ++nFrameCount;
                }
                TerminateShaders( &sShader );
            }
            TerminateEGL( eglDisplay, eglContext, eglSurface );
        }
        TerminateNativeSystem( &display, &window );
    }
 
    return 0;
}

リスト1. これから実装する処理の枠組み

プラットフォーム依存部の処理

プラットフォームに依存する部分の初期化と終了処理を実装します。ここでは主にウィンドウシステムの初期化などを行うことになります。何らかの初期化を行った結果を次のEGLの初期化の処理で使うことになるのですが、こちらはプラットフォームに依存しますので、それらしい空関数を入れておきます( リスト2 )。実装はお使いの環境に合わせて記述する流れとなります。
 


// -------- ここからリスト1で宣言/定義済み --------
// プラットフォーム共通化のために、それらしい型を定義しておく.
// 64ビット環境で、DisplayやWindowがint32_tの場合などが考えられるので、環境に合わせて適宜変更する.
typedef void* AppNativeDisplay;
typedef void* AppNativeWindow;
 
static bool InitializeNativeSystem( AppNativeDisplay* pNativeDisplay, AppNativeWindow* pNativeWindow );
static void PollNativeSystem( AppNativeDisplay* pNativeDisplay, AppNativeWindow* pNativeWindow );
static void TerminateNativeSystem( AppNativeDisplay* pNativeDisplay, AppNativeWindow* pNativeWindow );
// -------- ここまでリスト1で宣言/定義済み --------
 
// -------- ここからプラットフォーム依存部 --------
static bool InitializeNativeSystem( AppNativeDisplay* pNativeDisplay, AppNativeWindow* pNativeWindow )
{
    bool bRet = false;
 
    // -------- 何らかのプラットフォーム依存処理を実行する --------
    // 成功したらbRetをtrueに.
    // pNativeDisplayとpNativeWindowにはここで初期化したDisplayやWindowを格納.
 
    return bRet;
}
 
static void PollNativeSystem( AppNativeDisplay* pNativeDisplay, AppNativeWindow* pNativeWindow )
{
    if( pNativeDisplay && pNativeWindow ) {
        // -------- 何らかのプラットフォーム依存処理を実行する --------
        // 定例処理が必要な場合はここに実装する.
    }
}
 
static void TerminateNativeSystem( AppNativeDisplay* pNativeDisplay, AppNativeWindow* pNativeWindow )
{
    if( pNativeDisplay && pNativeWindow ) {
        // -------- 何らかのプラットフォーム依存処理を実行する --------
        // WindowやDisplayの破棄の処理を実装する.
    }
}
// -------- ここまでプラットフォーム依存部 --------

リスト2. プラットフォーム依存部分

EGLの初期化と終了処理

EGLの初期化と終了処理を実装します。いくつか関数を呼び出すだけではあるのですが、上記のプラットフォーム依存部で初期化されたAppNativeWindowとAppNativeDisplay型の変数を使います。
EGLの初期化部分では、まずは、引数で渡されたAppNativeDisplay型の変数からEGLDisplayを取得してEGLのシステムを初期化します。次に、これから作成したい描画面やEGLContextに関するオプションを記述し、EGLConfigを取得します。このEGLConfigを使い、描画面( EGLSurface )の作成、EGLContextの作成をそれぞれ行います。描画面の作成時には、AppNativeWindow型の変数も渡すことになります。
終了処理では、EGLContextの破棄、EGLSurfaceの破棄、そして、EGLのシステムの終了処理をそれぞれ行うAPIをコールする流れとなります。リスト3は上記の処理を記述したものになりますが、EGLがプラットフォーム依存部分と連携していることが感じられると思います。


// -------- ここからリスト1で宣言/定義済み --------
// プラットフォーム共通化のために、それらしい型を定義しておく.
// 64ビット環境で、DisplayやWindowがint32_tの場合などが考えられるので、環境に合わせて適宜変更する.
typedef void* AppNativeDisplay;
typedef void* AppNativeWindow;
 
static bool InitializeEGL( 
    AppNativeDisplay* pNativeDisplay, 
    AppNativeWindow* pNativeWindow, 
    EGLDisplay& display, EGLContext& context, EGLSurface& surface );
static void TerminateEGL( EGLDisplay& display, EGLContext& context, EGLSurface& surface );
// -------- ここまでリスト1で宣言/定義済み --------
 
// -------- ここからEGLの初期化と終了処理 --------
static bool InitializeEGL( 
    AppNativeDisplay* pNativeDisplay, 
    AppNativeWindow* pNativeWindow, 
    EGLDisplay& display, EGLContext& context, EGLSurface& surface )
{
    bool bRet = false;
 
    // 描画面に関する設定.この設定が使えるかどうかをChooseConfigする.
    EGLint aConfigAttrib[] = {
        EGL_BUFFER_SIZE, 16,
        EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
        EGL_NONE
    };
 
    // EGLContextを作るときに渡すオプション.OpenGL ES 2.0の場合は下記で固定.
    EGLint aCtxAttrib[] = {
        EGL_CONTEXT_CLIENT_VERSION, 2,
        EGL_NONE
    };
 
    EGLConfig config = nullptr;
    EGLint nNumConfig = 0;
 
    if( pNativeDisplay && pNativeWindow ) {
        // AppNativeDisplayからEGLDisplayを取得する.
        display = eglGetDisplay( reinterpret_cast(*pNativeDisplay) );
        if( EGL_NO_DISPLAY == display ) {
            std::cerr << "Error eglGetDisplay." << std::endl;
            return bRet;
        }
 
        // 取得したEGLDisplayでEGLのシステムを初期化.
        if( !eglInitialize( display, nullptr, nullptr ) ) {
            std::cerr << "Error eglInitialize." << std::endl;
            return bRet;
        }
        
        // OpenGL ESを使用すると宣言する.
        eglBindAPI( EGL_OPENGL_ES_API );
 
        // 上記で設定した描画面をサポートするEGLConfigがこのドライバにあるかどうかをチェック.
        if( !eglChooseConfig( display, aConfigAttrib, &config, 1, &nNumConfig ) ) {
            std::cerr << "Error eglChooseConfig." << std::endl;
            return bRet;
        }
        if( nNumConfig != 1 ) {
            std::cerr << "Error nNumConfig." << std::endl;
            return bRet;
        }
 
        // 上記EGLConfigが存在する場合は描画面を生成.
        // このときにAppNativeWindowを渡す.
        surface = eglCreateWindowSurface(
            display,
            config,
            reinterpret_cast( *pNativeWindow ),
            nullptr
        );
        if( EGL_NO_SURFACE == surface ) {
            std::cerr << "Error eglCreateWindowSurface. " << eglGetError() << std::endl;
            return bRet;
        }
 
        // 同様に上記で得られたEGLConfigを渡してEGLContextを作成する.
        context = eglCreateContext( display, config, EGL_NO_CONTEXT, aCtxAttrib );
        if( EGL_NO_CONTEXT == context ) {
            std::cerr << "Error eglCreateContext. " << eglGetError() << std::endl;
            return bRet;
        }
 
        // 最後に、このEGLSurfaceとEGLContextをアクティブなものにする..
        eglMakeCurrent( display, surface, surface, context );
        bRet = true;
    }
 
    return bRet;
}
 
static void TerminateEGL( EGLDisplay &display, EGLContext &context, EGLSurface &surface )
{
    eglDestroyContext( display, context );
    eglDestroySurface( display, surface );
    eglTerminate( display );
 
    display = EGL_NO_DISPLAY;
    context = EGL_NO_CONTEXT;
    surface = EGL_NO_SURFACE;
}
// -------- ここまでEGLの初期化と終了処理 --------

リスト3. EGLの初期化と終了処理
今回は、OpenGL ESでの簡単な図形描画の実践編の前半として、アプリケーションのメインフローと、プラットフォーム依存部分の仮実装、EGLの初期化部分についてご紹介しました。次回はいよいよ、シェーダや描画処理の本体部分を実装していきます。