おもちゃラボ

Unityで遊びを作ってます

OpenGLでゲームを作る 複数枚テクスチャを表示する

前回(OpenGLでゲームを作る テクスチャを表示する - おもちゃラボ)は一枚のテクスチャを表示しただけでしたが、実際にゲームでは複数枚のテクスチャを表示する必要があります。前回の内容がわかっていれば簡単だと思うので、確認程度に見て行きましょう。

const int g_windowWidth  = 640;
const int g_windowHeight = 480;
GLuint g_texIDs[2];

void render()
{
    static const GLfloat vtx[] = {
        -100, -100,
         100, -100,
         100,  100,
        -100,  100,
    };
    glVertexPointer(2, GL_FLOAT, 0, vtx);
    
    static const GLfloat texuv[] = {
        0.0f, 1.0f,
        1.0f, 1.0f,
        1.0f, 0.0f,
        0.0f, 0.0f,
    };
    glTexCoordPointer(2, GL_FLOAT, 0, texuv);
    

    // テクスチャの描画
    glEnable(GL_TEXTURE_2D);
    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);

    // 1枚目のテクスチャ描画
    glPushMatrix();
        glBindTexture(GL_TEXTURE_2D, g_texIDs[0]);
        glTranslatef(150, 250, 0);
        glDrawArrays(GL_QUADS, 0, 4);
    glPopMatrix();
    
    // 2枚目のテクスチャ描画
    glPushMatrix();
        glBindTexture(GL_TEXTURE_2D, g_texIDs[1]);
        glTranslatef(500, 250, 0);
        glDrawArrays(GL_QUADS, 0, 4);
    glPopMatrix();
    
    glDisableClientState(GL_TEXTURE_COORD_ARRAY);
    glDisableClientState(GL_VERTEX_ARRAY);
    glDisable(GL_TEXTURE_2D);
}

void setupTexture( GLuint texID, const char *file, const int width, const int height)
{
    // 画像データのロード
    std::ifstream fstr(file, std::ios::binary);
    assert(fstr);
    
    const size_t fileSize = static_cast<size_t>(fstr.seekg(0, fstr.end).tellg());
    fstr.seekg(0, fstr.beg);
    std::vector<char> textureBuffer(fileSize);
    fstr.read(&textureBuffer[0], fileSize);
    
    // 画像データとテクスチャiDを結びつける
    glBindTexture(GL_TEXTURE_2D, texID);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, &textureBuffer[0]);
    
    // テクスチャの各種設定
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
}


int main()
{
    if( !glfwInit() ){
        return -1;
    }
    
    if( !glfwOpenWindow(g_windowWidth, g_windowHeight, 0, 0, 0, 0, 0, 0, GLFW_WINDOW)) {
        return -1;
    }
    
    // モニタとの同期
    glfwSwapInterval(1);
    
    // 描画範囲の指定
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(0.0f, g_windowWidth, 0.0f, g_windowHeight, -1.0f, 1.0f);
    
    // テクスチャのロード
    glGenTextures(2, g_texIDs);
    setupTexture( g_texIDs[0], "sample.raw", 256, 256);
    setupTexture( g_texIDs[1], "sample2.raw", 256, 256);
    
    // ゲームループ
    while (glfwGetWindowParam(GLFW_OPENED)) {

        // 画面の初期化
        glClearColor(0.5f, 0.5f, 0.5f, 0.0f);
        glClear(GL_COLOR_BUFFER_BIT);
        
        render();
        
        // バッファの入れ替え
        glfwSwapBuffers();
    }
    
    glfwTerminate();
    
    return 0;
}

このプログラムを実行すると、下図のような画面が表示されると思います。変更点としては、テクスチャIDを管理する変数g_texIDsが配列になったことと、render関数の中で2枚のテクスチャを描画している部分ぐらいです。いやぁ〜テクスチャ一枚表示できてしまうと、後は簡単ですね〜。

f:id:nn_hokuson:20140211083942j:plain

複数枚テクスチャ表示の変更点

前回のプログラムから変更した点だけを見ていくことにします。まずは上にも書きましたが、今回は2枚のテクスチャを表示するので、テクスチャIDを管理する変数を配列にしています。g_texIDs[2];ってやつですね。

次に、OpenGLにお願いして空いているテクスチャIDを割り振ってもらいます。使用する関数はglGenTexturesでしたねー。今回は2つのテクスチャIDが欲しいので第一引数に2を指定しています。OpenGLから適当なテクスチャIDを割り振ってもらえたら、そのIDとテクスチャの画像データを対応付けます。今回はg_texIDs[0]のテクスチャIDとsample.rawを、g_texIDs[1]のテクスチャIDとsample2.rawを対応付けています。

最後にrender関数の中で2枚のテクスチャを描画しています。各テクスチャを描画する前にglBindTextureを呼び出し、引数としてこれから描画したいテクスチャのテクスチャIDを渡すことで任意のテクスチャを描画しています。前回はウインドウの中心にテクスチャを描画するため、頂点座標vtxに直接ウインドウの座標を指定していましたが、今回は原点でテクスチャを生成し、glTranslatefで移動させてから描画しています。移動の際にはglPushMatrix()とglPopMatrix()の間でglTranslatef()を呼び出すことで指定の位置まで移動させています。描画する位置の移動についてはOpenGLでゲームを作る 図形の描画 - おもちゃラボを参考にして下さい。


f:id:nn_hokuson:20140212233139j:plain


今回使用したRawファイルをココに置いておきます。
Rawファイル詰め合わせ

テクスチャ表示の高速化への道

さて、ここまで複数枚のテクスチャを描画する方法を説明してきました。今は2枚のテクスチャしか使用していないので、何の問題も無いように見えますが、描画するテクスチャの枚数が増えると、実は実行速度に少々問題がでてきます。その原因はglBindTexture関数で、この関数がめーっちゃくちゃ遅いのです。なので、glBindTexture関数のコール回数を出来るだけ下げることが必要になります。このあたりのお話は、OpenGL テクスチャレンダリングが遅い: 平成爆弾小僧でも紹介されていました。


そもそもなぜglBindTextureを複数回コールしていたかと言うと、2枚のテクスチャを描画する際にglBindTextureをコールしてテクスチャを切り替える必要があったからですねー。逆に言えば、1枚のでっかいテクスチャに必要な2枚の画像を書いておいて、それを切り出して使えばglBindTextureのコールは1回だけでいいのではねーか?というのが、この高速化の手法です。


f:id:nn_hokuson:20140212235713j:plain


一般的に、このように一枚のテクスチャに複数枚の画像をまとめたものを「テクスチャアトラス」と呼び、ゲームを作る際には良く使われる技術です。UnityやCocos2dなど、最近の有名なゲームエンジンであれば基本的にテクスチャアトラスはサポートされているとおもいます。テクスチャアトラスを利用するメリットとしては

  1. glBindTextureのコール回数を減らして高速化できる
  2. 画像をファイルからロードする回数を減らして高速化できる

といいコトづくしなので、利用しない手はありません(少々テクスチャをつくるのがめんどいですが・・・)それでは、テクスチャアトラスを利用して高速化したプログラムのrender関数を以下にのせます。プログラム全体は↓に置いておきます。

テクスチャ描画高速化プログラム

void render()
{
    static const GLfloat vtx[] = {
        -100, -100,
        100, -100,
        100,  100,
        -100,  100,
    };
    glVertexPointer(2, GL_FLOAT, 0, vtx);
    
    
    // テクスチャの画像指定
    glBindTexture(GL_TEXTURE_2D, g_texID);
    
    
    glEnable(GL_TEXTURE_2D);
    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);
    
    // テクスチャの描画1枚目
    static const GLfloat texuv[] = {
        0.0f, 1.0f,
        0.5f, 1.0f,
        0.5f, 0.0f,
        0.0f, 0.0f,
    };
    glTexCoordPointer(2, GL_FLOAT, 0, texuv);
    
    glPushMatrix();
    glTranslatef(150, 250, 0);
    glDrawArrays(GL_QUADS, 0, 4);
    glPopMatrix();
    
    // テクスチャの描画2枚目
    static const GLfloat texuv2[] = {
        0.5f, 1.0f,
        1.0f, 1.0f,
        1.0f, 0.0f,
        0.5f, 0.0f,
    };
    glTexCoordPointer(2, GL_FLOAT, 0, texuv2);

    glPushMatrix();
    glTranslatef(500, 250, 0);
    glDrawArrays(GL_QUADS, 0, 4);
    glPopMatrix();
    
    
    glDisableClientState(GL_TEXTURE_COORD_ARRAY);
    glDisableClientState(GL_VERTEX_ARRAY);
    glDisable(GL_TEXTURE_2D);
}

今回読み込むテクスチャは2枚のテクスチャを合成した横長のテクスチャ(textureAtras.raw)のため、setupTextureの引数には幅512と高さ256を渡しています。次にrender関数の中ですが、頂点座標を指定した後、glBindTextureでテクスチャアトラスを指定しています。今回使用するテクスチャは指定したテクスチャ1枚だけなので、glBindTextureをコールするのはこの一回だけでOKです。

複数の画像を一つのテクスチャにまとめて、テクスチャバインドを一回にする代わりに、テクスチャを描画する際のテクスチャ座標の指定を2回行っています。各テクスチャ座標を格納しているのがtexuvとtexuv2の配列になります。今回のテクスチャは以下のように半分の所で切れているため、テクスチャ座標のしていも0〜0.5と0.5〜1.0の2つになります。このへんは実際のプログラムと見比べてみてください。


f:id:nn_hokuson:20140213212454j:plain


各画像に対してテクスチャ座標を指定した後は、これまでと同じくglTranslateで移動させて描画します。このへんは上のプログラムと変わりませんね〜。

さらなる高速化への道

実は、もう1箇所プログラムの実行速度が遅くなる要因が隠されています。いわゆるドローコールと呼ばれる関数で、OpenGLの場合にはglDrawArrayに当たります。アプリケーションがglDrawArrayを呼ぶと、システムメモリ上に存在する頂点情報やテクスチャ情報などをドサッとGPUに転送します。その転送にともなってリソースの妥当性チェックも行われるのですが、この辺りのオーバーヘッドが大きいようです。詳しくは
DrawCallが9倍早くなるワケ | shikihuikuで解説されています。

ドローコールを減らすため、2つのオブジェクトの頂点情報とテクスチャ情報を、それぞれ1つの配列にまとめています。この方法の場合、それぞれの画像をglTranslatefやglRotatefで移動させることができないため、頂点配列を作成する時点で移動量も焼きこんでいます。 今回は2つの四角形オブジェクトを一度に描画するため、glDrawArrayの第三引数の頂点数を4から8へと変更しています。

今回もrender関数の部分のみを掲載します。完全なソースコードは↓に置いていますので利用して下さい。

テクスチャ描画高速化プログラム2

void render()
{
    static const GLfloat vtx[] = {
       -100+150, -100+250,
        100+150, -100+250,
        100+150,  100+250,
       -100+150,  100+250,
        
       -100+500, -100+250,
        100+500, -100+250,
        100+500,  100+250,
       -100+500,  100+250,
    };
    glVertexPointer(2, GL_FLOAT, 0, vtx);
    
    static const GLfloat texuv[] = {
        0.0f, 1.0f,
        0.5f, 1.0f,
        0.5f, 0.0f,
        0.0f, 0.0f,
        
        0.5f, 1.0f,
        1.0f, 1.0f,
        1.0f, 0.0f,
        0.5f, 0.0f,
    };
    glTexCoordPointer(2, GL_FLOAT, 0, texuv);
    
    // テクスチャの画像指定
    glBindTexture(GL_TEXTURE_2D, g_texID);
    
    // 一気にテクスチャを描画
    glEnable(GL_TEXTURE_2D);
    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);

    glDrawArrays(GL_QUADS, 0, 8);
    
    glDisableClientState(GL_TEXTURE_COORD_ARRAY);
    glDisableClientState(GL_VERTEX_ARRAY);
    glDisable(GL_TEXTURE_2D);
}

さて、この方法ですがglBindTextureもglDrawArrayも一度づつしかコールしないため高速化されているのですが、一方でボールやロケットなど、頻繁に移動する物体に対しては、頂点配列を再構築するオーバヘッドが必要なため、あまり効率的ではありません。逆にあまり移動しない背景のオブジェクトなどには非常に有効な手段だと思います。

このへんの高速化はUnityではドローコールバッチングとして実装されているようですねドローコールバッチング - Unity マニュアル


まとめ

今回は、複数のテクスチャを描画する方法を説明しました。複数のテクスチャを描画する際、ボトルネックとなるglBindTextureのコール回数を下げる方法としてテクスチャアトラスの説明をしました。また、ドローコールの回数を減らすため頂点配列とテクスチャ座標の配列に全てのオブジェクトの情報を記述する方法を紹介しました。

参考図書

「ゲームプログラマになる前に覚えておきたい技術」

この本は「ゲームをどのように作るのか」といった薄っぺらい内容ではなく、ゲームを動かすための技術を1から理解できるように解説してくれています。そして、この本にはゲーム開発にとどまらず、プログラマに必要な知識がたくさーん詰まっています。C++の基礎からキャッシュの話とか浮動小数点演算の話などのちょっと難しい話、最適化手法の話までもが書かれているので、プログラマならぜったい読むべき一冊ですね。そしてページ数も872ページとボリューミィ!分厚い参考書はいい本です!内部的に使われているのはDirectXですが、基本的にそれを意識する必要は内容に解説されているので、絶対に一度は読んでおきたい一冊です。

「OpenGLで作るiPhone SDKゲームプログラミング」

この本は2D〜3Dまでサンプルがとても充実しているので、ゲームを作りはじめようという時に非常に良い取っ掛かりになります!また、パーティクルや衝突判定などゲームには欠かせない話もしっかりと説明されています!