第5回 OpenVGを用いた簡単な図形の描画

カンデラの開発者による連載コラムです。 第5回は、「OpenVGを用いた簡単な図形の描画」です。

前回までの内容で、OpenGL ESを用いて簡単な図形は描画できるようになりました。では、似たような名称のOpenVGではどうなるのでしょうか。今回は、OpenVGで簡単な図形を描画することにチャレンジしてみましょう。

OpenVGで何かを描画する

OpenGL ESとOpenVGについては、それぞれプラットフォーム依存部分を吸収するレイヤーと してEGLというものを用いるというお話がありました( 図1 )。この基本構造についてはOpenGL ESの時と同様になりますので、前回までの内容を再利用できる部分があります。今回は基本的 にはOpenGL ES版を再利用、変更のある部分のみOpenVG向けに書き換えを行うという方針で 進めていきます。尚、今回の目標は図2のような三角形の描画になります。OpenVGには頂点毎 に色を決めるような方法は存在しませんので、単色の図形を描画することにしましょう。

図1. アプリケーションの構成図

 

図2. 今回の目標

構成

アプリケーションの構成としては図3の流れを想定とします。改修する必要がある部分について は図3で、〇〇色の枠で囲まれた項目となります。特筆すべき点としては、OpenGL ES版の構成 図から、シェーダ関連の処理が抜け落ちていることが挙げられます。これはOpenVGがコマンドを ベースで図形構成して描画する仕組みとなっているためです。詳しくは三角形の準備の部分でご 紹介します。
 

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

エントリポイント

OpenGL ES版で、Shaderに関する構造体を定義したり、それを渡して処理したりする関数がありましたが、こちらはOpenVGに適応する形で、削除や変更を行っています。それ以外の部分については差分はありませんので、説明は割愛します。

処理の枠組み( ソースコードを見る )

// プラットフォーム共通化のために、それらしい型を定義しておく.
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 void Draw( SFigureData* pData, EGLDisplay& display, EGLSurface& surface );
static void DrawTriangle( SFigureData* pData );
static void DestroyTriangle( SFigureData* pData );
 
 
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;
 
    SFigureData sFigure;
    sFigure.path = VG_INVALID_HANDLE;
    sFigure.paint = VG_INVALID_HANDLE;
 
    if( InitializeNativeSystem( &display, &window ) ) {
        if( InitializeEGL( &display, &window, eglDisplay, eglContext, eglSurface ) ) {
            VGint nFrameCount = 0;
            while( nFrameCount < NUM_FRAMES ) {
                PollNativeSystem( &display, &window );
 
                Draw( &sFigure, eglDisplay, eglSurface );
                ++nFrameCount;
            }
            DestroyTriangle( &sFigure );
            TerminateEGL( eglDisplay, eglContext, eglSurface );
        }
        TerminateNativeSystem( &display, &window );
    }
 
    return 0;
}

リスト1. 処理の枠組み

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

プラットフォームに依存する部分の初期化と終了処理についてもOpenGL ES版と同様になりますので説明は割愛します。実装はお使いの環境に合わせて記述する流れとなります。  

プラットフォーム依存部分( ソースコードを見る )

// -------- ここからリスト1で宣言/定義済み --------
// プラットフォーム共通化のために、それらしい型を定義しておく.
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. プラットフォーム依存部分( OpenGL ES版と同様 )

EGLの初期化と終了処理

こちらは、図3内の編集対象部分に相当しますが、修正箇所は2か所のみです。まずは描画面を生成する際に設定するEGLConfigの中にEGL_RENDERABLE_TYPEというオプションがあります。こちらが、OpenGL ES版の場合、EGL_OPENGL_ES2_BITとしていましたが、今回はOpenVGを使いますので、EGL_OPENVG_BITとします。さらに、これからOpenVGを用いるということをEGL側に知らせるために、eglBindAPI関数をEGL_OPENVG_APIを渡してコールします。これらの内容以外につきましては、OpenGL ES版と同様になります。

EGLの初期化と終了処理( ソースコードを見る )

// -------- ここからリスト1で宣言/定義済み --------
// プラットフォーム共通化のために、それらしい型を定義しておく.
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_OPENVG_BIT,
        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_OPENVG_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, nullptr );
        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の初期化と終了処理( 一部修正 )

描画する三角形の準備

次に描画する三角形の準備をします。OpenVGの座標系は、図4のようになります。左下が原点、右上に向かって座標が正の向きになります。右上が、描画面の解像度になります。今回は図5のような座標で三角形を描画します。今回は、OpenVGの描画の例としてよく用いられる閉じた図形に色を付ける方法で描画を行います。イメージとしては図6のように、形状のデータと、塗りつぶしのデータを準備することになります。


図4. OpenVGの座標系
 

図5. 今回描画する三角形の頂点
 

図6. 準備するデータ

形状の準備

形状データ生成の基本的な流れとしては、コマンド( 今回の例の場合TRIANGLE_SEGMENTに含まれているMOVE_TO_ABSやLINE_TO_ABSなど )とそこに渡すためのデータ( 今回の例の場合はTRIANGLE_POINTS )を定義して、それらの情報をVGPathオブジェクトに追加していくことになります。今回はアニメーションなどは行いませんので、一度作成した形状データはクリアや再生成などは行わず、そのまま保持します。この段階では、必要な定数群だけ用意しておき( リスト4 )実際の生成処理は描画ループ内でまとめて行うことにします。

形状データの定義( ソースコードを見る )

// このソースファイルでしか使わない変数群なので無名名前空間に逃がす.
namespace {
    // 必要なコマンド数は、始点への移動と残りの点へ線を引くことと、PathをCloseすること.
    const int32_t NUM_TRIANGLE_SEGMENT = 4;
    // MOVE_TO_ABSやLINE_TO_ABSに渡す座標は、3点 x 2( xとy ).
    const int32_t NUM_TRIANGLE_BUFFER_LENGTH = 6;
    
    // コマンドの列挙.
    const VGubyte TRIANGLE_SENGMENT[NUM_TRIANGLE_SEGMENT] = {
        VG_MOVE_TO_ABS, 
        VG_LINE_TO_ABS, 
        VG_LINE_TO_ABS, 
        VG_CLOSE_PATH 
    };
 
    // コマンドに渡す座標.
    const VGfloat TRIANGLE_POINTS[NUM_TRIANGLE_BUFFER_LENGTH] = {
        160.0f, 120.0f, 
        480.0f, 120.0f, 
        320.0f, 360.0f 
    };
}

リスト4. 形状データの定義

塗りつぶしの準備

単色の塗りつぶしについては、VGPaintオブジェクトを生成し、それに対して色情報をセットするという流れになります。形状データの準備同様、ここでは、後に必要となる定数のみ準備しておき( リスト5 )、実際の生成処理は描画ループ内で行うことにします。

塗りつぶし色の定義( ソースコードを見る )

// このソースファイルでしか使わない変数群なので無名名前空間に逃がす.
namespace {
    // 色の指定はRGBAの順で、[ 0.0f, 1.0f ]となるように指定する。要素数は4.
    const int32_t NUM_COLOR_LENGTH = 4;
    const VGfloat TRIANGLE_COLOR[NUM_COLOR_LENGTH] = { 0.0f, 0.75f, 0.0f, 1.0f };
}

リスト5. 塗りつぶし色の定義

形状データと塗りつぶしデータを保持するための準備

前述の2種類のデータを保持しておくための構造体を定義します。今回はリアルタイムでの変形は行いませんので、VGPathとVGPaintを保持する構造体SFigureを定義しておきます( リスト6 )。描画ループ内で行う予定の生成処理では、この構造体のメンバに値を代入することになります。

データ保持のための構造体( ソースコードを見る )

typedef struct _SFigureData {
    VGPath path;
    VGPaint paint;
} SFigureData;

リスト6. データ保持のための構造体

描画ループ

リスト1の時点で、ひな形だけは存在している描画ループですが、ここではその中身を作り込みます。OpenVGも、OpenGL ES同様、前フレームの描画結果を一度クリアし、今フレームの内容を描き込むという手順を踏むのが一般的です。ですので、描画ループの頭の部分では、まず描画面のクリアを行います。そして、描画命令を発行し、最後に描画面を入れ替えという、OpenGL ESと同様の手順を踏みます。

今回は、形状データと塗りつぶしデータが未生成の場合、初回描画時に生成するという流れとしています。また、プラットフォーム固有の何らかの処理を毎フレーム呼ぶ必要がある場合はその内容も記述することになります。それを想定して、リスト2内のPollNativeSystem関数が描画ループからコールされています。ここまでの内容が、リスト7にまとめられています。

描画ループの内部処理( ソースコードを見る )

// このソースファイル内でしか使わない定数は無名名前空間に逃がす.
namespace {
    // これまで無名名前空間に定義されていたものはすべてマージされているものとする.
    // ( 略 ).
 
    const int32_t NUM_FRAMES = 600;                 // 指定回ループが回ったらこのプログラムを抜ける.
    
    // 描画面のクリア色.
    const VGfloat CLEAR_COLOR[NUM_COLOR_LENGTH] = { 0.25f, 0.25f, 0.5f, 1.0f };
}
 
// -------- ここからリスト1で宣言/定義済み --------
static void Draw( SFigureData* pData, EGLDisplay& display, EGLSurface& surface );
static void DrawTriangle( SFigureData* pData );
static void DestroyTriangle( SFigureData* pData );
// -------- ここまでリスト1で宣言/定義済み --------
 
static void Draw( SFigureData* pData, EGLDisplay& display, EGLSurface& surface )
{
    // 描画面を指定色でクリアする.
    vgSetfv( VG_CLEAR_COLOR, NUM_COLOR_LENGTH, CLEAR_COLOR );
    vgClear( 0, 0, VIEWPORT_WIDTH, VIEWPORT_HEIGHT );
 
    // 三角形の描画処理本体.
    DrawTriangle( pData );
 
    // 裏面と表面を入れ替える.
    eglSwapBuffers( display, surface );
}
 
static void DrawTriangle( SFigureData* pData )
{
    if( pData ) {
        // もしVGPathがまだ作られていない場合は作成する.
        if( VG_INVALID_HANDLE == pData->path ) {
            /** 
            * Pathデータのハンドルを作成.
            * フォーマットはSTANDARD、点のデータは浮動小数( VGfloat ).
            * Scaleは1倍、Bias( オフセット )は指定しない.
            * セグメント数、頂点数に関するヒントは与えない( ドライバ任せにする ).
            * Pathの用途は特に制限をかけない( PATH_CAPABILITY_ALL ).
            */
            pData->path = vgCreatePath(
                VG_PATH_FORMAT_STANDARD, VG_PATH_DATATYPE_F,
                1.0f, 0.0f, 0, 0,
                VG_PATH_CAPABILITY_ALL 
            );
 
           /** 
            * セグメント、頂点を上記で作成したVGPathにセット.
            * セグメント数を指定し、対応するセグメントと頂点の配列を渡す.
            */
            vgAppendPathData( 
                pData->path, 
                NUM_TRIANGLE_SEGMENT, 
                TRIANGLE_SENGMENT, 
                TRIANGLE_POINTS 
            );
        }
 
        // もしVGPaintがまだ作られていない場合は作成する.
        if( VG_INVALID_HANDLE == pData->paint ) {
            // まずはVGPaintを作成する.
            pData->paint = vgCreatePaint();
 
            // ここで作成したVGPaintは単色で塗りつぶすものであると指定する.
            vgSetParameteri( 
                pData->paint, 
                VG_PAINT_TYPE, 
                VG_PAINT_TYPE_COLOR 
            );
 
            // VGPaintに色を指定する.
            vgSetParameterfv( 
                pData->paint, 
                VG_PAINT_COLOR, 
                NUM_COLOR_LENGTH, 
                TRIANGLE_COLOR
            );
        }
 
        // 形状、塗りつぶし、いずれのデータも揃っている場合は描画命令を発行.
        if( pData->path && pData->paint ) {
            vgSetPaint( pData->paint, VG_FILL_PATH );
            vgDrawPath( pData->path, VG_FILL_PATH );
        }
    }
}
 
static void DestroyTriangle( SFigureData* pData )
{
    if( pData ) {
        // 塗りつぶしデータの破棄.
        if( pData->paint ) {
            vgDestroyPaint( pData->paint );
            pData->paint = VG_INVALID_HANDLE;
        }
        // 形状データの破棄.
        if( pData->path ) {
            vgDestroyPath( pData->path );
            pData->path = VG_INVALID_HANDLE;
        }
    }
}
 
// リスト1のmain関数( 一部省略 ).
int main( int argc, char *argv[] )
{
    // 変数宣言など省略.
    ...
 
    // 略.
    if( ... ) {
        if( ... ) {
            // 指定回ループが回ったらアプリケーションを抜ける.
            VGint nFrameCount = 0;
            while( nFrameCount < NUM_FRAMES ) {
                // Nativeの何らかの定例処理.
                    PollNativeSystem( &display, &window );
 
                // 描画処理.
                Draw( &sFigure, eglDisplay, eglSurface );
                ++nFrameCount;
            }
            ...
        }
        ...
    }
 
    return 0;
 }

リスト7. 描画ループの内部処理

ここまでのまとめ

ここまでご紹介した内容を統合すると図1のような三角形を描画することができます。OpenGL ES版よりは手順が少ないように感じたかもしれません。三角形以外の図形も、MOVE_TOやLINE_TOなどの命令を駆使することで描画することができます。また楕円など特定の図形の描画にはそれに特化した別のコマンドが用意されていることもあります。OpenVGに関しては、英語、日本語のいずれも資料がそれほど豊富ではありませんので、何か別の図形の描画を試してみる場合は、OpenVGの仕様書を確認するのが近道となります。

OpenVGを用いた図形の描画に関するご紹介は以上となります。EGLというレイヤが存在しているおかげで、OpenGL ES版の処理の大部分を流用できたと思います。実際には今回のような基本的な矩形の描画に加えて、画像の描画、グラデーションの表現、曲線や自由な形状の描画などを駆使し実用的な画面を作成していくことになります。

 

次回は、特定のグラフィックAPIに依存しない、ソフトウェアレンダリングに挑戦してみたいと思います。