OpenGL ES2(2) - まずは初期化 (2012/09/15)

Raspberry Pi を使った OpenGL ES2 のプログラミングを解説します。そもそも Raspberry Pi はコンピュータサイエンスや電子工学の学習を目的として開発されているようですから、ここでも基板むき出しの Raspberry Pi に合わせて「生々しいプログラミング」をしてみたいと思います。グラフィック系のアプリケーションはオブジェクト指向と相性が良く、C++ を選択するほうがキレイなプログラムになりますが、「どっかで誰かがうまい事やってくれている」感が出てしまうので、少なくとも初めは自分で作った感じになる C言語 (正式名称は「プログラミング言語C 」)を使います。

前回も書きましたが、OpenGL ES2 には固定機能と呼ばれる、行列演算、色、法線ベクトル、カメラ、照明などに関する機能がなく、OpenGL ES2用に誰かが書いたライブラリを利用するのでなければ、すべて自分で作成する必要があります。 では OpenGL ES2 は何をするものかというと、頂点の座標変換や頂点が持つ情報の補間と塗りつぶしを超高速でやってくれるシェーダプログラムをGPUで実行するための機能を持っています。 最近の CPU は2コアとか4コアが普通ですが、GPUは数十から1000個のコアが同時に高速に処理を行います。 Raspberry Pi の GPU は48個のコアを持っているようです。

初めは行列演算などを使用しない2次元のプログラムを作成します。次のスクリーンショットの三角形が左上に向かって移動するだけのプログラムです。

まずは Raspberry Pi 専用の初期化部分を中心にまとめてみました。今後毎回使うことになる呪文のようなコードになります。 今回は OpenGL ES2 の命令(関数)は1行もありません。OpenGL ES2はプラットフォームに依存しない(はずの)APIですが、ハードウェアやOSによって初期化する内容に違いがあります。Raspberry Pi専用の初期化を把握すれば、後は書籍やWeb上のサンプルをRaspberry Pi上で動作させられるようになると思います。

Raspberry Pi公式イメージのサンプル

Raspberry PiのOS、Raspbianには /opt/vc/以下のディレクトリに VideoCore 用のライブラリ、ヘッダファイル、サンプルプログラムが用意されています。現状ではドキュメントが無いに等しいので使い方はサンプルプログラムとヘッダファイルから推測するしかありません。

/dev/vchiq のパーミッション

OpenGL ES2 のアプリケーションを実行するにはデバイスファイルの /dev/vchiq に対する権限が必要です。 所有者は root、グループは video になっています。video グループには pi ユーザのみが属しています。別にユーザを作成して使っている方は /etc/group を編集して video グループにユーザを追加して下さい。

Raspberry Pi専用の初期化

Raspberry Pi で OpenGL ES2を使う場合には、一番最初に一回だけ「 bcm_host_init();」を実行する必要があります。普通は main 関数の先頭付近に置けばいいでしょう。

ヘッダファイルは Raspberry Pi に依存する部分も含めて以下の2つでOKです。ヘッダファイルの実体は /op/vc/include 以下に格納されています。

#include <GLES2/gl2.h>
#include <EGL/egl.h>

初期化の過程で設定または取得する値をまとめて管理するために次のような構造体を定義して使用しています。

typedef struct {
  EGLNativeWindowType  nativeWin;
  EGLDisplay  display;
  EGLContext  context;
  EGLSurface  surface;
  EGLint      majorVersion;
  EGLint      minorVersion;
  int         width;
  int         height;
} ScreenSettings;

どんな環境でも、最初に描画するウィンドウを指定する事から始めます。 Windowsのウィンドウや X11、MacOSX のウィンドウ、iOS などでそれぞれ異なる方法が必要となります。 Windows の場合はウインドウハンドル(hWin)が必要になるようですが、Raspberry Pi では以下に示す EGL_DISPMANX_WINDOW_T 構造体へのポインタを取得することになります(この部分は気にしなくても大丈夫)。

typedef struct {
   DISPMANX_ELEMENT_HANDLE_T element;
   int width;
   int height;
} EGL_DISPMANX_WINDOW_T;

Raspberry Pi ではコンソール画面の一部または全体に OpenGL ES2 による描画を行うことになります。 コマンドラインの文字が表示されたコンソール画面にオーバーレイするような形式でグラフィックを表示します。 初期化で必要な処理は実行されているディスプレイのサイズを取得し、 グラフィックを描画する領域を左上の座標と描画領域の幅と高さで指定します。次のコードでスクリーン全体を描画領域にしています。

// 環境依存のネイティブなウィンドウを取得 (2012/09/25 修正)
EGLBoolean WinCreate(ScreenSettings *sc)
{
  uint32_t success = 0;
  uint32_t width;
  uint32_t height;
  VC_RECT_T dst_rect;
  VC_RECT_T src_rect;
  DISPMANX_ELEMENT_HANDLE_T dispman_element;
  DISPMANX_DISPLAY_HANDLE_T dispman_display;
  DISPMANX_UPDATE_HANDLE_T dispman_update;
  static EGL_DISPMANX_WINDOW_T nativewindow;
  VC_DISPMANX_ALPHA_T alpha = {DISPMANX_FLAGS_ALPHA_FIXED_ALL_PIXELS, 255, 0};

  // フルスクリーンの表示サイズを取得
  success = graphics_get_display_size(0, &width, &height);
  if (success < 0) return EGL_FALSE;

  sc->width = width;
  sc->height = height;
  // 表示サイズ、位置、拡大率の設定
  vc_dispmanx_rect_set(&dst_rect, 0, 0, sc->width, sc->height);
  vc_dispmanx_rect_set(&src_rect, 0, 0, sc->width << 16, sc->height << 16);

  dispman_display = vc_dispmanx_display_open(0);
  dispman_update = vc_dispmanx_update_start(0);
  dispman_element = vc_dispmanx_element_add(dispman_update, dispman_display,
             0, &dst_rect, 0, &src_rect, DISPMANX_PROTECTION_NONE, &alpha, 0, 0);
  vc_dispmanx_update_submit_sync(dispman_update);
  nativewindow.element = dispman_element;
  nativewindow.width = width;
  nativewindow.height = height;
  sc->nativeWin = &nativewindow;
  return EGL_TRUE;
}

上のコード中の vc_dispmanx_rect_set 関数は VC_RECT_T 構造体のメンバを 設定するだけの関数で、VC_RECT_T 構造体は /opt/vc/include/interface/vctypes/vc_image_types.h で定義されていて 次のように四角形の領域の左上の座標と幅、高さをメンバとして持っています。

typedef struct tag_VC_RECT_T {
   int32_t x;
   int32_t y;
   int32_t width;
   int32_t height;
} VC_RECT_T;

src_rect と dst_rect の幅と高さを異なるもので指定すると表示を拡大または縮小する事が出来ます。 画面に表示されるのは dst_rect で指定した位置とサイズになります。

下のコードは描画領域の幅と高さを src_rect に対して320 x 240のサイズで設定し、 それを dst_rect.x と dst_rect.y で指定した位置 (画面中央) にサイズを4倍に拡大して表示する場合の例です。 この例では vc_dispmanx_rect_set 関数を使用せずに VC_RECT_T 構造体に値を直接設定しています。

  sc->width = 320;                            // 拡大縮小前の幅指定
  sc->height = 240;                           // 拡大縮小前の高さ指定

  dst_rect.x = (width - sc->width * 4) / 2;   // 左表示位置の指定
  dst_rect.y = (height - sc->height * 4) / 2; // 上表示位置の指定
  dst_rect.width = sc->width * 4;             // 幅は拡大縮小可能
  dst_rect.height = sc->height * 4;           // 高さも拡大縮小可能

  src_rect.x = 0;                     // 値を変えても表示内容は変わらない
  src_rect.y = 0;                     // 値を変えても表示内容は変わらない
  src_rect.width = sc->width << 16;           // 物理表示サイズの幅指定
  src_rect.height = sc->height << 16;         // 物理表示サイズの高さ指定

EGLによる初期化

WinCreate で描画領域に関する情報を取得したら eglGetConfigs で Raspberry Piで使用できる画面モード(後述) の中から eglChooseConfig で表示に使用するモードを指定します。 関数の先頭部で行っている配列の初期化でRGBA各8ビット、 深さバッファ24ビットを指定していますが、 普通はこの設定のままで十分ではないかと思います。

// ネイティブなウィンドウにEGLで描画領域を設定

EGLBoolean SurfaceCreate(ScreenSettings *sc)
{
  EGLint attrib[] = {
    EGL_RED_SIZE,       8,  // 赤のビット数
    EGL_GREEN_SIZE,     8,  // 緑のビット数
    EGL_BLUE_SIZE,      8,  // 青のビット数
    EGL_ALPHA_SIZE,     8,  // 透明度のビット数
    EGL_DEPTH_SIZE,     24, // デプスバッファのビット数
    EGL_NONE
  };
  EGLint context[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};
  EGLint numConfigs;
  EGLConfig config;

  sc->display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
  if (sc->display == EGL_NO_DISPLAY) return EGL_FALSE;
  if (!eglInitialize(sc->display, &sc->majorVersion, &sc->minorVersion))
    return EGL_FALSE;
  if (!eglChooseConfig(sc->display, attrib, &config, 1, &numConfigs))
    return EGL_FALSE;
  sc->surface = eglCreateWindowSurface(sc->display, config, sc->nativeWin, NULL);
  if (sc->surface == EGL_NO_SURFACE) return EGL_FALSE;
  sc->context = eglCreateContext(sc->display, config, EGL_NO_CONTEXT, context);
  if (sc->context == EGL_NO_CONTEXT) return EGL_FALSE;
  if (!eglMakeCurrent(sc->display, sc->surface, sc->surface, sc->context))
      return EGL_FALSE;
  return EGL_TRUE;
}

main関数

以上の初期化に関する関数である WinCreate と SurfaceCreate を使う部分の main関数は次のようになります。

#include <GLES2/gl2.h>
#include <EGL/egl.h>

typedef struct {
  EGLNativeWindowType  nativeWin;
  EGLDisplay  display;
  EGLContext  context;
  EGLSurface  surface;
  EGLint      majorVersion;
  EGLint      minorVersion;
  int         width;
  int         height;
} ScreenSettings;

  略

ScreenSettings  g_sc;

int main ( int argc, char *argv[] )
{
  bcm_host_init();
  res = WinCreate(&g_sc);
  if (!res) return 0;
  res = SurfaceCreate(&g_sc);
  if (!res) return 0;
  :
  略
  :
}

Raspberry Piで使用できる Config 一覧

eglGetConfigs でRaspberry Piで使用できるモードをすべて取得して表にまとめてみました。 各モードで共通の値は最後にまとめてあります。RGB全体で16ビットのモード、RGBの各色が8ビットの24ビットモード、透明度の8ビットが追加された32ビットの3グループに大別できます。各項目の詳細はKHRONOSのサイトで EGL 1.4 Specification が公開されていますから、参照して下さい。

R5G6B5A0

赤の5ビット、緑の6ビット、青の5ビットで1ピクセルあたり2バイトを使用するモードです。1色あたり32階調(赤青)と64階調(緑)の計65,536色の表示が可能です。

Config ID 20 27 19 28 18 17 24 22 21 23
Red Size 5 5 5 5 5 5 5 5 5 5
Green Size 6 6 6 6 6 6 6 6 6 6
Blue Size 5 5 5 5 5 5 5 5 5 5
Alpha Size 0 0 0 0 0 0 0 0 0 0
Buffer Size 16 16 16 16 16 16 16 16 16 16
Depth Size 0 0 0 16 24 24 0 24 24 0
Stencil Size 0 0 8 0 0 8 0 0 8 8
Samples 0 0 0 0 0 0 4 4 4 4
Sample Buffers 0 0 0 0 0 0 1 1 1 1
Bind to Texure RGB 1 1 1 1 1 1 0 0 0 0
Bind to Texure RGBA 1 1 1 1 1 1 0 0 0 0
Alpha Mask Size 0 8 0 0 0 0 0 0 0 0
Conformant 7 7 7 7 7 7 5 5 5 5
Native Visual ID 107544107544107544107544107544107544107544107544107544107544

R8G8B8A0

赤の8ビット、緑の8ビット、青の8ビットで1ピクセルあたり3バイトを使用するモードです。赤、青、緑とも256階調の計16,777,216色で表示できます。

Config ID 8 26 6 4 2 16 14 12 10
Red Size 8 8 8 8 8 8 8 8 8
Green Size 8 8 8 8 8 8 8 8 8
Blue Size 8 8 8 8 8 8 8 8 8
Alpha Size 0 0 0 0 0 0 0 0 0
Buffer Size 24 24 24 24 24 24 24 24 24
Depth Size 0 0 0 24 24 0 0 24 24
Stencil Size 0 0 8 0 8 0 8 0 8
Samples 0 0 0 0 0 4 4 4 4
Sample Buffers 0 0 0 0 0 1 1 1 1
Bind to Texure RGB 1 1 1 1 1 0 0 0 0
Bind to Texure RGBA 1 1 1 1 1 0 0 0 0
Alpha Mask Size 0 8 0 0 0 0 0 0 0
Conformant 7 7 7 7 7 5 5 5 5
Native Visual ID 33832 33832 33832 33832 33832 33832 33832 33832 33832

R8G8B8A8

赤の8ビット、緑の8ビット、青の8ビット、透明度8ビットで1ピクセルあたり4バイトを使用するモードです。赤、青、緑とも256階調の計16,777,216色で表示できます。 各ピクセルで256階調の透明度を設定できます。

Config ID 7 25 5 3 1 15 13 11 9
Red Size 8 8 8 8 8 8 8 8 8
Green Size 8 8 8 8 8 8 8 8 8
Blue Size 8 8 8 8 8 8 8 8 8
Alpha Size 8 8 8 8 8 8 8 8 8
Buffer Size 32 32 32 32 32 32 32 32 32
Depth Size 0 0 0 24 24 0 0 24 24
Stencil Size 0 0 8 0 8 0 8 0 8
Samples 0 0 0 0 0 4 4 4 4
Sample Buffers 0 0 0 0 0 1 1 1 1
Bind to Texure RGB 0 0 0 0 0 0 0 0 0
Bind to Texure RGBA 1 1 1 1 1 0 0 0 0
Alpha Mask Size 0 8 0 0 0 0 0 0 0
Conformant 7 7 7 7 7 5 5 5 5
Native Visual ID 37928 37928 37928 37928 37928 37928 37928 37928 37928

共通の項目

Max pBuffer Height 2048
Max pBuffer Width 2048
Max pBuffer Pixels 4194304
Level 0
Native Renderable 1
Min Swap Interval 0
Max Swap Interval 2147483647
Color Buffer Type 12430
Surface Type 1639
Transparent Type 12344
Transparent Blue Value 0
Transparent Green Value 0
Transparent Red Value 0
Config Caveat 12344
Native Visual Type 12344
Luminance Size 0
Renderable Type 7

まとめ

以上で Raspberry Pi 特有の初期化に関する部分は終了です。 Raspbianの公式ディスクイメージの /opt/vc/src/hello_pi/hello_triangle ディレクトリに テクスチャを貼った立方体が回転するサンプルソースが格納されています。 init_ogl関数でRaspberry Pi 特有の dispman の初期化、EGLの初期化、 OpenGL ES2の初期化が混ざって行われていますが、今回は Raspberry Pi 特有部分の初期化(WinCreate) と EGLの初期化(SurfaceCreate)を分けています。複雑そうに見えますが、毎回はじめに使う呪文と思って使えばいいと思います。


前のページ 概要

続く...