もんしょの巣穴blog

OpenGL ES 2.0 その02

1日1HaloWars状態のMonshoです。
さすがに慣れてきたのでヒロイックでも余裕です。
そろそろいろいろ縛りプレーでもしようかな。

GLES2.0プログラムその02はシェーダコンパイルです。
なぜいきなりシェーダなのかというと、GLES2.0には固定シェーダがないからです。
単色トライアングルを出すだけでもシェーダを書かなければいけません。
GLES2.0のシェーダは頂点シェーダとフラグメントシェーダに分かれています。
頂点シェーダはそのままの意味で、フラグメントシェーダはピクセルシェーダのことです。
ピクセルシェーダといった方がわかりやすいかもしれませんが、ここではフラグメントシェーダと言うことにしましょう。
今回使用するシェーダプログラムは以下の通りです。

// 頂点シェーダコード
attribute highp vec4 myVertex;
uniform mediump mat4 myPMVMatrix;
void main(void)
{
    gl_Position = myPMVMatrix * myVertex;
}

// フラグメントシェーダコード
void main (void)
{
    gl_FragColor = vec4(1.0, 1.0, 0.66 ,1.0);
}

簡単に説明しましょう。
頂点シェーダは入力頂点座標 myVertex を入力行列 myPMVMatrix で変換しています。
これを gl_Position に出力していますが、この変数は出力頂点座標となります。ここからポリゴンが画面のどこに描画されるか計算するわけです。
フラグメントシェーダは gl_FragColor に定数カラーを出力しています。実際に画面に出てくる色がこの変数です。
このコードは外部のテキストファイルに書き込んでおいてもかまいません。最終的にプログラム内部で文字列としてメモリに格納されていればOKです。
テキストファイルを読み込む場合、文字列の最後が '' で終わるようにしておきましょう。当たり前のことですが、結構忘れやすいです。>忘れたやつがここにいるしね

これらは文字列としてメモリに格納されている状態とします。ここからシェーダプログラムを作成します。
まず、各シェーダコードをコンパイルします。
コンパイルは頂点シェーダもフラグメントシェーダも同じ要領で行えます。なので、ここでは頂点シェーダをコンパイルするコードのみ提示します。

const char* pVertexCode;  // ここにシェーダコードが入っていると思いねぇ
GLuint hShader;

// シェーダオブジェクトを作成する
hShader = glCreateShader( GL_VERTEX_SHADER );

// シェーダコードを読み込む
// シェーダオブジェクト、コードの数、シェーダコード、シェーダコードの長さの配列
glShaderSource( hShader, 1, &pVertexCode, NULL );

// シェーダコードをコンパイルする
glCompileShader( hShader );

順番に見ていきましょう。
シェーダコードは pVertexCode に入っているものと考えてください。
まず、glCreateShader() でシェーダを作成し、そのハンドルを取得します。
引数はシェーダタイプで、GL_VERTEX_SHADER なら頂点シェーダ、GL_FRAGMENT_SHADER ならフラグメントシェーダです。フラグメントシェーダをコンパイルする場合、この引数を変えるだけでOKです。
次に glShaderSource() でシェーダコードを登録します。複数のシェーダコードを読み込むこともできるらしいのですが、ハンドルは1つでシェーダコードは複数ってどういう状況なんでしょう?
普通はハンドル1つにシェーダコード1つでしょうし、複数のシェーダを読み込むことはないと思います。なので、第2引数は 1 でOKです。
第3引数はシェーダコード。文字列の先頭アドレスではなく、格納場所のポインタなのは複数のシェーダを読み込むためです。
第4引数はシェーダコードの文字数ですが、'' で終わっているなら必要ありません。終わっていないならやはり文字数のポインタを指定します。
で、glCompileShader() でコンパイルします。が、この関数は戻り値が void なのでうまくいったのかどうかはここではわかりません。
というか、OpenGLのほとんどの関数は戻り値が void なので、エラーは別途関数を用いて取得する必要があります。
大半の関数のエラーは glGetError() で取得できるのですが、シェーダコンパイルについてはエラーメッセージもあるのでそんなに単純ではありません。以下がコンパイルエラーの検出方法です。

// コンパイルが成功しているかどうか調べる
GLint bShaderCompiled;
glGetShaderiv( hShader, GL_COMPILE_STATUS, &bShaderCompiled );
if( !bShaderCompiled )
{
    // コンパイルエラー
    // エラーログのメッセージ長を取得
    int  nLogLength, nCharsWritten;
    glGetShaderiv( hShader, GL_INFO_LOG_LENGTH, &nLogLength );

    // エラーログを取得
    char* pInfoLog = new char[ nLogLength ];
    glGetShaderInfoLog( hShader, nLogLength, &nCharsWritten, pInfoLog );

    // エラーログ出力
    printf( "%s
", pInfoLog );

    delete[] pInfoLog;
}

glGetShaderiv() は指定のシェーダハンドルに関するいろいろな情報を取得できる関数です。
最初に GL_COMPILE_STATUS を指定しています。これでコンパイルが成功しているかどうかを取得します。値が 0 ならコンパイルは失敗です。
次に GL_INFO_LOG_LENGTH でエラーログの文字列の長さを取得しています。この長さは文字列の終端も含めた文字数が入ってきます。
この文字数でエラーログの格納領域を作成したら glGetShaderInfoLog() でエラーログを文字列として取得できます。適当に表示しましょう。
第3引数で実際に書き込まれたログの文字数が取得できるのですが、あまり必要なものとは思えませんね。

このようにして頂点シェーダとフラグメントシェーダをコンパイルすることに成功したら、今度は2つのシェーダをリンクして一つのプログラムを作成します。

GLuint hProgram;

// シェーダプログラムを作成する
hProgram = glCreateProgram();

// フラグメント、頂点シェーダをアタッチする
glAttachShader( hProgram, hFragment );
glAttachShader( hProgram, hVertex );

// プログラムをリンクする
glLinkProgram( hProgram );

// リンクに成功したかどうか調べる
GLint bLinked;
glGetProgramiv( hProgram, GL_LINK_STATUS, &bLinked );
if( !bLinked )
{
    // リンクエラー
    // エラーログのメッセージ長を取得
    int  nLogLength, nCharsWritten;
    glGetProgramiv( hProgram, GL_INFO_LOG_LENGTH, &nLogLength );

    // エラーログを取得
    char* pInfoLog = new char[ nLogLength ];
    glGetProgramInfoLog( hProgram, nLogLength, &nCharsWritten, pInfoLog );

    // エラーログ出力
    printf( "%s
", pInfoLog );

    delete[] pInfoLog;
}

まず、glCreateProgram() でシェーダプログラムを作成します。
次に、glAttachShader() を使って頂点シェーダとフラグメントシェーダをアタッチします。
glLinkProgram() でリンクすれば終了です。簡単ですね。
こちらもシェーダコンパイルと同様にエラーログを取得できます。やり方もシェーダコンパイルとほぼ同じです。関数名とかがちょっと違うだけですね。

作成したプログラムを実際に使用するには以下の関数を呼び出せばOKです。

glUseProgram( hProgram );

これでプログラムが使用できるようになります。

と、このようにそれほど難しい流れではないので、適当に関数化なりクラス化しておくと楽でしょう。
次回はやっとポリゴン描画に行けます。トライアングルを出すだけですけどね。

スポンサーサイト
  1. 2009/02/14(土) 16:51:30|
  2. プログラミング
  3. | トラックバック:0
  4. | コメント:0

mapやsetを大量に使うのはありなのか?

ここ最近遊んだデモと言えば、PS3の『KILLZONE2』とXbox360の『Halo Wars』。
KZ2はコンシューマFPSとしては最高峰のグラフィックで、特にモーションはずば抜けていると言っていいでしょう。
GoW1みたいに色が薄い、と言うかモノクロ調なので派手さは感じませんが、さすがに待たせただけあるものにはなってます。
ただ、もっとカバーオブジェクトとか破壊できるものと思ってたんですが、全然破壊できず。
体験版だからなのか、そういうステージなのかはわかりませんが、ちょっとがっかり。
あと、操作性が悪すぎる。半分はPS3パッドの所為とは言え、半分は間違いなくゲームの所為。
カバーアクションとかもただしゃがんでるだけとしか感じられない。ボタン押しっぱなしなのも個人的に×。
リアリティを追求した結果かも知れませんが、私は遊びやすいゲームの方が好きです。
対照的に遊びやすかったのがHWの方。まさか、コンシューマRTSがこんなに遊びやすくなるとは思ってませんでした。
確かに所々「マウスとキーボードの方が」と思う部分はありますが、ほぼ不満がない出来映えです。
ゲーム自体もうまい具合に簡略化されていて、Halo世界の表現としても秀逸。
Haloは好きだけどRTSはちょっと…という人でも体験してみるといいと思います。
問題は、初心者には難しすぎて、上級者には簡単すぎるかもしれないと言うこと。
RTS初心者をうまく取り込めればいいですが、そうでなければ上級者に底が浅いと言われる可能性も否定できません。
しかし、体験版としては成功したのではないかと思います。

今日は思うところがあってGameProgrammingGemsの1章を中心に読んでいたんですが、Gems5の1.3 コンポーネントベースのオブジェクト管理がちょっと面白かったので簡単に紹介。
ゲーム中のオブジェクトを管理する伝統的な方法としては、たとえばCObjectみたいな基底クラスを用意し、すべてのオブジェクトはそこから派生させる、と言うものだと思います。
つまり、シーンはCObject*をvectorとかで管理するわけです。
問題は、あるオブジェクトに今までなかった機能を追加しようとする場合に発生します。
記事にも書いてありますが、たとえばアニメーションしないはずのオブジェクトがアニメーションすることになったとすると、そのオブジェクトは継承ツリーの別の場所に移動させる必要が出てきます。
また、その機能がそもそも存在しない場合、どこかのクラスにその機能を追加させ、そのクラスから派生しているすべてのオブジェクトにその機能の実装を求める必要が出てきます。
これは結構面倒ですし、ゲーム開発末期にそんな話が出てきたりするとかなり危険です。
そこで、機能ごとにコンポーネントを作成し、オブジェクトとコンポーネントを動的に関連づけることで継承による問題をなくしてしまおうというもの。
オブジェクト、コンポーネント、インターフェースの関係は以下のような1つのデータベースで管理されます。

struct SObjectManagerDB
{
    SComponentTypeInfo     mComponentTypeInfo[NUM_COMPONENT_TYPE_IDS];
    std::set<EComponentTypeId>   mInterfaceTypeToComponentTypes[NUM_INTERFACE_IDS];
 
    std::map<CObjectId, IComponent*> mComponentTypeToComponentMap[NUM_COMPONENT_TYPE_IDS];
 
    std::set<EComponentTypeId>   mMessageTypeToComponentTypes[NUM_MESSAGE_TYPES];
};

オブジェクトとコンポーネントの関係は mComponentTypeToComponentMap で管理されます。
たとえば、キャラクタの体力を示すHPコンポーネントを考えます。
EnemyオブジェクトはPlayerオブジェクトのHPをチェックし、その行動を変化させるものと考えてください。
まず、Enemyオブジェクトは mComponentTypeToComponentMap からHPコンポーネントのIDに対応するmapを取得します。mComponentTypeToComponentMap[ CID_HP ] って感じで。
その中からPlayerオブジェクトIDに対応するコンポーネントインターフェースを取得します。
NULL以外が返ってくればコンポーネントは見つかったので、そこからHPの値を取得することができます。
また、メッセージディスパッチは mMessageTypeToComponentTypes から指定メッセージを受け取るべきコンポーネントIDのsetを取得し、やっぱり mComponentTypeToComponentMap からmapを受け取ります。
特定のオブジェクトならそのmapからコンポーネントを取得し、不特定の相手ならそのmapすべてのコンポーネントにメッセージを送るというわけです。
オブジェクトはコンポーネントという形の能力を持っていて、その能力に従ってメッセージを受けたり処理を行ったりすると言うわけです。
しかし、個人的に気になったのはsetやmapをかなり使っていると言うこと。
コンポーネントの数が増えれば mComponentTypeToComponentMap の数が増えます。
メッセージが増えれば mMessageTypeToComponentTypes の数が増えます。
どちらも10個くらい、とかならともかく、どんどん大きくなってきたらメモリ管理が大変そうだと思ったり思わなかったり。
っていうか、そもそもゲーム本体の中でsetやmapってそんなに使うかなぁ?
もちろん、ピンポイントで使用するならそれほど問題ないですが、この技術のように配列で大量に使用するっていうのはいまいち納得できないというか…。
技術的には面白いだけに、別の解決策は模索できないのかなぁと思ったりするわけで。
皆さんはどう思います?

  1. 2009/02/11(水) 23:41:06|
  2. プログラミング
  3. | トラックバック:0
  4. | コメント:0

OpenGL ES 2.0 その01

大往生、2月か。侍道3とHaloWarsもあるというのに…。
とはいえ、大往生はすでにAmazonで予約済み。デススマイルズの限定版はすでに品切れ(泣。
さて、スティックをどうするか考えないと。

と言うわけでGLES2.0を使ってみようのコーナー、第1回。
前回はイントロダクション、って感じで。本格的に今回から。
本格的に、とか言っても今回は初期化のみ。別のサンプル作ってて時間がなくなったので。

GLESの初期化にはEGLライブラリを使用します。
EGLライブラリはネイティブ・プラットフォーム・グラフィクス・インターフェイス。つまり、OSやらハードウェアやらとの間を取り持ってくれるライブラリです。
基本的に、ウィンドウに関する処理はほぼこいつがやってくれます。実際、今回はこれだけです。
Windows用エミュレータでGLESを使用する場合、まずはウィンドウを作成します。これはDirectXを使う場合と変わりません。
ただ、DirectXはウィンドウハンドル(HWND)があれば良かったのですが、GLESではデバイスコンテキストが必要になります。なので、これを取得します。

HDC    hDC = GetDC( hWnd );

次にこのデバイスコンテキストからGL用ディスプレイを作成します。

EGLDisplay    eglDisplay = eglGetDisplay( hDC );
if( eglDisplay == EGL_NO_DISPLAY )
{
    // 失敗した時用
    eglDisplay = eglGetDisplay( (EGLNativeDisplayType)EGL_DEFAULT_DISPLAY );
}

EGLDisplay は void* の typedef です。他にも void* の typedef がいくつかありますが、void* を使用するのは避けましょう。当たり前ですが。
ではOpenGLを初期化しましょう。

// GLを初期化する
if( !eglInitialize( eglDisplay, &majorVersion, &minorVersion ) )
{
    return false;
}

初期化の際にバージョンを取得できます。いらないのであれば NULL を指定してもかまいません。

// コンフィグ
const EGLint pi32ConfigAttribs[] =
{
    EGL_LEVEL,    0,
    EGL_SURFACE_TYPE,  EGL_WINDOW_BIT,
    EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
    EGL_NATIVE_RENDERABLE, EGL_FALSE,
    EGL_DEPTH_SIZE,   EGL_DONT_CARE,
    EGL_NONE
};

// コンフィグ選択
EGLConfig eglConfig;
int  nConfigs;
if( !eglChooseConfig( eglDisplay, pi32ConfigAttribs, &eglConfig, 1, &nConfigs ) || (nConfigs != 1) )
{
    return false;
}

この命令は指定のコンフィグに見合ったフレームバッファ構成を取得します。
コンフィグは1次元配列ですが、実際には2つで1つです。つまり、偶数番目が項目、奇数番目がその値という形で、終端の偶数番目にEGL_NONEを置きます。
指定の構成が見つかれば nConfigs に見つかった構成の数が入ってきます。今回は1つだけならOKという形を取っています。

// サーフェイスを作成する
EGLSurface eglSurface = eglCreateWindowSurface( eglDisplay, eglConfig, eglWindow, NULL );
if( eglSurface == EGL_NO_SURFACE )
{
    return false;
}

EGLSurface も void* の typedef です。eglWindow は中身は HWND です。ネイティブ環境のウィンドウハンドルを入れておく必要があります。

// APIをバインドする
eglBindAPI( EGL_OPENGL_ES_API );

// コンテキストを作成する
EGLint ai32ContextAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE };
EGLContext eglContext = eglCreateContext( eglDisplay, eglConfig, NULL, ai32ContextAttribs );
if( !TestEGLError( hWnd, "eglCreateContext" ) )
{
    return false;
}

レンダリングコンテキストを作成します。
コンテキスト作成前にAPIにOpenGLESを指定します。OpenVG(だっけ?)とかも選択できますが、今回必要なのはGLESです。
eglCreateContext() の第4引数はコンフィグの時と同じようなものを指定します。
第3引数は別のコンテキストを指定します。すると、そのコンテキストとテクスチャを共有することができます。
DirectXで言えば異なる2つのIDirect3DDeviceでテクスチャを共有できるようになるものと考えてください。

// ディスプレイ、サーフェイス、コンテキストを関連づける
eglMakeCurrent( eglDisplay, eglSurface, eglSurface, eglContext );
if( !TestEGLError( hWnd, "eglMakeCurrent" ) )
{
    return false;
}

ディスプレイ、サーフェイス、コンテキストを関連づけます。これで初期化は終了。
eglMakeCurrent() の第2,3引数は draw と read のサーフェイスを指定します。同じものを指定してもかまわないようです。
draw は描画されるサーフェイスなんですが、read は読み出し可能なサーフェイスってことかな?この辺がよくわかりません。
終了処理はデバイスコンテキストとウィンドウハンドルを解放すればOK。

// クリアカラーの設定
glClearColor( 0.6f, 0.8f, 1.0f, 1.0f );
// 画面クリア
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT );

// スワップ
eglSwapBuffers( eglDisplay, eglSurface );

画面に表示する処理はこんな感じ。当然、クリアからスワップまでの間で描画を行います。
DirectXと違って、画面クリアする前にクリア設定を行う必要があります。
一度設定すれば変更するまで設定を変える必要はありません。
楽と言えば楽かもしれませんが…実際にはあんまり意味ないですよね。
深度やステンシルの初期値もあらかじめ設定しておきます。

とまあ、適当すぎる解説で恐縮ですが、私も細部はよくわかってないもので。
少なくとも、こうすればたいていの環境では動くはずです。たぶん。

  1. 2009/02/01(日) 19:39:31|
  2. プログラミング
  3. | トラックバック:0
  4. | コメント:0

プロフィール

monsho

Author:monsho
ゲームプログラマ?

最近の記事

最近のコメント

最近のトラックバック

月別アーカイブ

カテゴリー

ブロとも申請フォーム

この人とブロともになる

ブログ内検索

RSSフィード

リンク

このブログをリンクに追加する