読者です 読者をやめる 読者になる 読者になる

おもちゃラボ

Unityで遊びを作っていきます

【OpenGLでゲームを作る】テクスチャを表示する

今回は、前回作った四角形のポリゴンにテクスチャを貼り付けて画像を表示します。2Dゲームではテクスチャを貼り付けた四角形のポリンゴンをスプライトと呼びます。

f:id:nn_hokuson:20170224103602p:plain:w200

テクスチャを表示するのはこれまでと比べると少し大変ですが、頑張っていきましょう。

テクスチャを描画する流れ

OpenGLでテクスチャを表示する流れは次のとおりです。まずはテクスチャの表示がどのような流れになるのかを掴んでおきましょう。

  1. 表示したい画像をファイルから読み込みます。
  2. 読み込んだ画像データのどの部分を使いたいのかをテクスチャ独自の座標系「uv座標」を使って指定します。
  3. このuv座標のデータをOpenGLからバーテックスシェーダ経由でフラグメントシェーダに渡します。
  4. また、画像のピクセルデータを直接フラグメントシェーダに渡します。
  5. フラグメントシェーダ内でテクスチャを表示します。

f:id:nn_hokuson:20170224104359p:plain:w500

これだけ書くと、思ったよりも簡単だと思うかもしれません。でも、そんなことはない(笑)上では画像ファイルを読み込みます、と簡単に描きましたが、読み込んだ画像データをOpenGLに登録したり、テクスチャの使い方を指定したりと結構やることはあります。

ソースコードの全容は一番下に記載しています。まずは画像を読み込む部分から説明していきますね。

画像ファイルを読み込む

OpenGLでは下図のように、テクスチャ画像をテクスチャIDという数値(GLuint型)で管理します。OpenGL内部ではテクスチャID⇔テクスチャデータの対応付けが行われており、テクスチャIDを使って使用するテクスチャを指定します。

f:id:nn_hokuson:20170224105622p:plain:w500

まずはglGenTexturesを使って、空いてるテクスチャID番号を調べます。glGenTexturesの第一引数には欲しい「空きテクスチャID」の個数を入れます。第二引数に指定した変数に、空きテクスチャIDが関数内で格納されます。(複数のテクスチャIDを教えてもらいたい場合には第二引数に配列を渡します)

 glGenTextures(1, &texID);
関数名 void glGenTextures(GLsizei n, GLuint* textures)
引数 n:生成するテクスチャ数
textures:テクスチャIDの配列
戻り値 なし

 
続けて、画像のピクセルデータをtextureBuffer配列にロードしています。C++でファイル読み込みをする際のいつもの書き方ですね。今回は、画像ロードのプログラムが極力短く簡単になるようにraw形式の画像ファイル(うちの猫)を読み込んでいます。

 string filename = "cat.raw";
 std::ifstream fstr(filename, std::ios::binary);
            
 const size_t fileSize = static_cast<size_t>(fstr.seekg(0, fstr.end).tellg());
 fstr.seekg(0, fstr.beg);
 char* textureBuffer = new char[fileSize];
 fstr.read(textureBuffer, fileSize);

Raw画像を作る方法はこちらで紹介しています。

nn-hokuson.hatenablog.com

簡単に試したい場合は次のURLからもダウンロードできます。
https://app.box.com/s/5vp7pp3339n3m7g6indr0uu7mirerskn
 
次にglBindTextureとglTexImage2Dの2つの関数を利用して、テクスチャデータをGPUのVRAMにアップロードします。まず、glBindTexture関数を使って、これから指定したテクスチャIDの操作を行うことをOpenGLに伝えます。続けてglTexImage2Dに、先ほどロードしたtextureBufferを引数として渡すことで、指定したテクスチャIDのテクスチャデータをGPUのVRAMにアップロードします。

f:id:nn_hokuson:20170224165718p:plain:w450

プログラムは次の通りです。こglTexImage2Dの第四・五引数には画像の縦横サイズを指定します。また、第三・七引数には画像ファイルのチャネル数(今回のRAWファイルはアルファレイヤを含まないのでGL_RGB)を指定します。

 glBindTexture(GL_TEXTURE_2D, texID);
 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 256, 256, 0, GL_RGB, GL_UNSIGNED_BYTE, textureBuffer);
関数名 void glBindTextures(GLenum target, GLuint texture)
引数 target:テクスチャの種類
texture:テクスチャID
戻り値 なし


最後にglTexParameteriを使ってテクスチャの各種設定を行っています。具体的にはテクスチャ表示時のデータの補間方法と、テクスチャ座標外のテクスチャの扱い方を指定しています。

 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
関数名 void glTexParameteri(GLenum target, GLenum name, Glint param)
引数 target:テクスチャの種類
pname:変更するパラメータ名
param:変更する値
戻り値 なし

さて、ここまででようやくOpenGLでテクスチャを使う準備が整いました。テクスチャの細かい設定などは「マルチプラットフォームのためのOpenGL ES入門」に非常に詳しく書いてあります。参考にしてみて下さい。

マルチプラットフォームのためのOpenGL ES入門 基礎編―Android/iOS対応グラフィックスプログラミング

マルチプラットフォームのためのOpenGL ES入門 基礎編―Android/iOS対応グラフィックスプログラミング

シェーダでテクスチャを表示する

商用ゲームではメモリの節約や、実行速度の高速化などの理由から、複数のテクスチャをまとめたテクスチャアトラスを使用します。テクスチャアトラスを使う場合はテクスチャの一部を切り取って使用するので、テクスチャの領域指定が必要になります。

f:id:nn_hokuson:20170224113619p:plain:w400

どの部分を使用するかは、テクスチャの左下を原点とするuv座標系で指定します。今回はテクスチャの全面を使用するので、0 ≦ u ≦ 1, 0 ≦ v ≦ 1 の範囲で指定します。

f:id:nn_hokuson:20170224113838p:plain:w250

頂点座標と対応するようにテクスチャ座標も指定して下さい。つまり、頂点座標は左下の(-0.5, -0.5)から反時計回りに指定しているので、テクスチャ座標も左下の(0, 1)から反時計回りに指定しています。

 const GLfloat vertex_uv[] = {
      1, 1,
      0, 1,
      0, 0,
      1, 0,
 };

OpenGLで定義したuvデータをバーテックスシェーダで受け取り、フラグメントシェーダに渡す部分を作ります。

これは、前回やった頂点色データをバーテックスシェーダで受け取り、フラグメントシェーダに渡したのと同じやり方です。

nn-hokuson.hatenablog.com

まずはuv座標データを受け取るattribute変数と、フラグメントシェーダに渡すvarying変数を定義しています。そしてバーテックスシェーダの中でvarying変数にattribute変数を代入しています。

 attribute vec3 position;
 attribute vec2 uv;
 varying vec2 vuv;
 void main(void){
     gl_Position = vec4(position, 1.0);
      vuv = uv;
 }

次にフラグメントシェーダではバーテックスシェーダから渡されたvarying変数を受け取る変数を用意しています。また、テクスチャのピクセルデータを受け取るためのuniform変数を定義しています。

フラグメントシェーダの本体ではGLSLに用意されているtexture関数を使ってuv変数で指定した一のピクセル情報を取り出しています。取り出したデータはgl_FragColorに代入して出力しています。

varying vec2 vuv;
uniform sampler2D texture;
void main(void){
       gl_FragColor = texture2D(texture, vuv);
}

テクスチャをシェーダに渡す

最後に、OpenGLからシェーダにuv座標データとピクセルデータを渡す部分を書きましょう。uv座標を渡す流れは毎度おなじみの次の流れです。

  1. glGetAttribLocation関数を使ってattribute変数の番号を取得
  2. glEnableVertexAttribArray関数を使ってattribute変数を有効化
  3. glVertexAttribPointerでデータを渡す
int uvLocation = glGetAttribLocation(programId, "uv");
glEnableVertexAttribArray(uvLocation);
glVertexAttribPointer(uvLocation, 2, GL_FLOAT, false, 0, vertex_uv);

ピクセルデータはUniform変数なのでglGetUniformLocation関数を使ってuniform変数の番号を調べて、glUniform1i関数を使ってシェーダにデータを渡しています。glUniform1iには第一引数にuniform変数の番号、第二引数に使用するテクスチャIDを指定します。

int textureLocation = glGetUniformLocation(programId, "texture");
glUniform1i(textureLocation, 0);

ソースコードと実行結果

今回のソースコードの主要な部分は次のようになります。ソースコードの全文はGitHubにアップしているので、合わせて確認してみて下さい。

GLuint loadTexture(string filename)
{
    // テクスチャIDの生成
    GLuint texID;
    glGenTextures(1, &texID);
    
    // ファイルの読み込み
    std::ifstream fstr(filename, std::ios::binary);
    const size_t fileSize = static_cast<size_t>(fstr.seekg(0, fstr.end).tellg());
    fstr.seekg(0, fstr.beg);
    char* textureBuffer = new char[fileSize];
    fstr.read(textureBuffer, fileSize);
    
    // テクスチャをGPUに転送
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
    glBindTexture(GL_TEXTURE_2D, texID);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 256, 256, 0, GL_RGB, GL_UNSIGNED_BYTE, textureBuffer);
    
    // テクスチャの設定
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    
    // テクスチャのアンバインド
    delete[] textureBuffer;
    glBindTexture(GL_TEXTURE_2D, 0);
    
    return texID;
}

int main()
{
    if( !glfwInit() ){
        return -1;
    }
    
    GLFWwindow* window = glfwCreateWindow(g_width, g_height, "Simple", NULL, NULL);
    if (!window)
    {
        glfwTerminate();
        return -1;
    }
    
    // モニタとの同期
    glfwMakeContextCurrent(window);
    glfwSwapInterval(1);
    
    GLuint programId = crateShader();
    
    GLuint texID = loadTexture("cat.raw");
    
    // ゲームループ
    while (!glfwWindowShouldClose(window)) {
        
        // 画面の初期化
        glClearColor(0.2f, 0.2f, 0.2f, 0.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        glClearDepth(1.0);
        
        // 頂点データ
        float vertex_position[] = {
            0.5f, 0.5f,
            -0.5f, 0.5f,
            -0.5f, -0.5f,
            0.5f, -0.5f
        };
        
        const GLfloat vertex_uv[] = {
            1, 0,
            0, 0,
            0, 1,
            1, 1,
        };
        
        // 何番目のattribute変数か
        int positionLocation = glGetAttribLocation(programId, "position");
        int uvLocation = glGetAttribLocation(programId, "uv");
        int textureLocation = glGetUniformLocation(programId, "texture");
        
        // attribute属性を有効にする
        glEnableVertexAttribArray(positionLocation);
        glEnableVertexAttribArray(uvLocation);
        
        // uniform属性を設定する
        glUniform1i(textureLocation, 0);

        // attribute属性を登録
        glVertexAttribPointer(positionLocation, 2, GL_FLOAT, false, 0, vertex_position);
        glVertexAttribPointer(uvLocation, 2, GL_FLOAT, false, 0, vertex_uv);      
    
        // モデルの描画
        glBindTexture(GL_TEXTURE_2D, texID);
        glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
        
        // バッファの入れ替え
        glfwSwapBuffers(window);
        
        // イベント待ち
        glfwPollEvents();
    }
    
    glfwTerminate();
    
    return 0;
}

■ソースコード全文はこちら
texture.cpp · GitHub
 
実行結果は次のとおりです。

f:id:nn_hokuson:20170305223323p:plain:w350

まとめ

今回はようやくテクスチャの表示までできました。かなり大変でしたが、ここが山場です(たぶん)。次回は座標変換のお話をしたいと思います。