おもちゃラボ

Unityで遊びを作ってます

OpenGLでゲームを作る 図形の描画

OpenGLで図形を描画する

今回は、ようやくOpenGLを使って画面上に図形を描画してみたいと思います。図形と言っても今回は複雑な3Dモデルというわけではなく、点や線、三角形や四角形を描画していきます。この中でも特に、三角形は複雑な3Dモデル(ポリゴン)を構成する基本単位です。逆に言えば三角形さえ描けてしまえば、どんなに複雑なように見えるキャラクタも作れるわけです(実際には、もちろんMayaとかメタセコイアとかを使って作りますけどね・・・)

f:id:nn_hokuson:20140119083333j:plain

上に書いたウサギは、スタンフォードバニー(Stanford Bunny)といって、画像処理のレナさんと同じぐらい有名な人(?)です。まぁ、それはさておき、以下がOpenGLを使って画面上に点と線と三角形、四角形を描画するプログラムになります。

#include <GL/glfw.h>
#include <cstdlib>

const int g_windowWidth  = 640;
const int g_windowHeight = 480;

void render()
{
    // 点
    static const GLfloat vtx1[] = { 90.0f, 250.0f };
    glVertexPointer(2, GL_FLOAT, 0, vtx1);
    glPointSize(4.0f);
    glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
    
    glMatrixMode(GL_MODELVIEW);
    glEnableClientState(GL_VERTEX_ARRAY);
    glDrawArrays(GL_POINTS, 0, 1);
    glDisableClientState(GL_VERTEX_ARRAY);
    
    // 線
    static const GLfloat vtx2[] = {
        180.0f, 200.0f,
        180.0f, 300.0f,
    };
    glVertexPointer(2, GL_FLOAT, 0, vtx2);
    glLineWidth(4.0f);
    glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
    
    glEnableClientState(GL_VERTEX_ARRAY);
    glDrawArrays(GL_LINES, 0, 2);
    glDisableClientState(GL_VERTEX_ARRAY);
    
    // 三角
    static const GLfloat vtx3[] = {
        320.0f, 300.0f,
        260.0f, 200.0f,
        390.0f, 200.0f,
    };
     
    glVertexPointer(2, GL_FLOAT, 0, vtx3);
    glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
     
    glEnableClientState(GL_VERTEX_ARRAY);
    glDrawArrays(GL_TRIANGLES, 0, 3);
    glDisableClientState(GL_VERTEX_ARRAY);
    
    // 四角形
    static const GLfloat vtx4[] = {
        450.0f, 300.0f,
        450.0f, 200.0f,
        550.0f, 200.0f,
        550.0f, 300.0f,
    };
    
    glVertexPointer(2, GL_FLOAT, 0, vtx4);
    glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
    
    glEnableClientState(GL_VERTEX_ARRAY);
    glDrawArrays(GL_QUADS, 0, 4);
    glDisableClientState(GL_VERTEX_ARRAY);
}

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);
    
    // ゲームループ
    while (glfwGetWindowParam(GLFW_OPENED)) {

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


今回はrender関数の中身だけを追記しています。描画している図形は点、線、三角形、四角形と様々ですが、各図形の描画プログラムは非常に似ていることが分かると思います。各図形とも描画の流れとしては

  1. 頂点座標を配列として定義する
  2. glVertexPointerでOpenGLに頂点座標を伝える
  3. glDrawArrayでバックバッファに対して描画する

となります。

 

glVertexPointer

glVertexPointerの1つ目の引数には頂点の頂点データのサイズを指定します。頂点データサイズとは、頂点データが持つ座標情報の数です。2次元であれば 2 を、3次元なら3 を指定します。2つ目の引数には頂点配列の型を指定します。ここではfloat型で頂点配列を作成しているのでGL_FLOATを与えています。3つ目の引数には、頂点配列の間隔を指定します。今回は0を指定しています。4つ目の引数には頂点配列へのポインタを指定します。

glDrawArray

glDrawArrayの1つ目の引数には図形の種類を指定しています。2つ目の引数には描画を始める頂点配列のインデックスを指定します。3つ目の引数は描画する頂点の数を指定します。点なら1、線なら2、三角形なら3,四角形なら4ですね。

f:id:nn_hokuson:20140119142531j:plain

描画する座標の指定方法を考えなおす

ここまで、画面上に図形を描画してきましたが、図形を描画する座標は頂点配列として直打ちして指定しました。このままでは、図形を移動、回転、拡大させたい場合に柔軟性が低く、全ての頂点を手で計算する必要があります。そこで、「図形は原点に描画しておき、行列を使って座標変換することで描画座標を移動させる」という方式に変更してみましょう。

//
// 原点に100x100の正方形を描画する関数
//
void drawSquare()
{
    static const GLfloat vtx4[] = {
        -50.0f,  50.0f,
        -50.0f, -50.0f,
         50.0f, -50.0f,
         50.0f,  50.0f,
    };
    
    glVertexPointer(2, GL_FLOAT, 0, vtx4);
    glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
    
    glEnableClientState(GL_VERTEX_ARRAY);
    glDrawArrays(GL_QUADS, 0, 4);
    glDisableClientState(GL_VERTEX_ARRAY);
}

void render()
{
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    glPushMatrix();
        glTranslatef( 320.0f, 240.0f, 0.0f );
        drawSquare();
    glPopMatrix();
}

今回は、原点に100x100の大きさの正方形を描画するdrawSquare関数を用意しました。中身は先ほどのrender関数の四角形描画部分と殆ど同じで、変更点としては原点を中心に±50の座標をしている部分だけです。また、render関数内には、四角形を移動させるプログラムを書いています。まずは図形を移動させたいので、GL_MODELVIEWのスタックを指定して、glLoadIdentityでスタックをクリアしています。このへんの座標変換のお話は前回の「【OpenGLでゲームを作る】ダブルバッファリングとは - おもちゃラボ」をご参照ください。
 
glPushMatrix()とglPopMatrix()に挟まれた部分に、図形の移動と描画のコードを書いています。原点にある正方形を画面の中心座標(320, 240)に移動させるため、glTranslatef関数を使っています。引数は頭からx方向の移動量、y方向の移動量、z方向の移動量です。

glPushMatrixとglPopMatrixのお話

本日最後のお話は先ほど説明を飛ばしたglPushMatrixとglPopMatrixです。これらの関数はそれぞれ、MODELVIEWのスタックの状態(スタックの一番上の行列)をコピーしスタックの一番上にプッシュする関数と、プッシュした行列をポップする関数になります。ここまでの例だと、なぜこれらの関数が必要になるかわかりにくいと思います。


そこで、複数の図形を描画する場合を考えてみましょう。例えば、(200, 240)と(400, 240)に四角形を描画するために、以下のプログラムを書いたとしましょう。この場合どのような描画結果になるでしょうか?この場合の描画結果は下図のように、(100, 240)と(600, 480)に四角形が描かれてしまいます。

void render()
{
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    // (200, 240)に四角形を描く
    glTranslatef( 200.0f, 240.0f, 0.0f );
    drawSquare();

    // (400, 240)に四角形を描く(つもり)
    glTranslatef( 400.0f, 240.0f, 0.0f );
    drawSquare();
}

f:id:nn_hokuson:20140119143215j:plain

どうしてこのような結果になってしまったのか、MODEL_VIEWの行列スタックの様子を見ながら考えてみましょう。まず、最初にglTranslatef( 200.0f, 240.0f, 0.0f )の移動行列がスタックに積まれます。この状態で1つ目の四角形が描画されるので、一つ目の四角形は正しく(200, 240)に描画されます。その後 glTranslatef( 400.0f, 240.0f, 0.0f )の移動行列がスタックに投げ込まれ、現在の行列に掛け合わされます。その結果、MODEL_VIEWの行列スタックの一番上には(600, 480)への移動行列が積まれることになります。この状態で2つ目の四角形の描画が行われるため、2つ目の四角形は(600, 480)に描画されてしまうことになります。

f:id:nn_hokuson:20140119084415j:plain

これを防ぐためには、2つ目のglTranslatefの引数を(200.0f, 0.0f, 0.0f)にすればよいのですが、これはMODEL_VIEWの行列スタックの状態をアプリケーションが管理しなくてはいけなくなるので、全然イケていません。そこで、考えだされたのがglPushMatrixとglPopMatrixです。これらを使用してプログラムを書くと以下のようになり、正しく描画されていることがわかります。

void render()
{
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    // (200, 240)に四角形を描く
    glPushMatrix();
        glTranslatef( 200.0f, 240.0f, 0.0f );
        drawSquare();
    glPopMatrix();
    
    // (400, 240)に四角形を描く
    glPushMatrix();
        glTranslatef( 400.0f, 240.0f, 0.0f );
        drawSquare();
    glPopMatrix();
}

f:id:nn_hokuson:20140119143236j:plain

最後に、このときのMODEL_VIEWの行列スタックの状態がどのように変化するのかを見てみましょう。まず最初にglPushMatrixが呼ばれた時点で、現在の行列スタックのトップの変換行列(単位行列)がコピーされスタックにプッシュされます。このプッシュされた行列に対してglTranslatef( 200.0f, 240.0f, 0.0f )の移動行列が掛け合わされます。この状態で、1つ目の四角形が描画されるので、四角形は(200, 240)に描画されることになります。その後、glPopMatrixが呼ばれ、行列スタックの一番上の変換行列がポップされ、行列スタックの状態はプッシュする前と同じものになります。

同様に2回目のglPushMatrixが呼ばれ、行列スタックのトップがプッシュされます。このプッシュされた行列に対してglTranslatef( 400.0f, 240.0f, 0.0f )の移動行列が掛け合わされます。この状態で、2つ目の四角形が描画されるので、四角形は(400, 240)に描画されることになります。その後、変換行列がポップされることで、行列スタックの状態はの元に戻ります。

f:id:nn_hokuson:20140119084436j:plain

まとめ

今回は、ようやくウインドウに対して描画を行えました。また、glPushMatrixとglPopMatrixを使って直感的に座標指定が出来る方法を説明しました。少々ややこしかったかもしれませんが、ここらへんの話は階層化したオブジェクトを動かす場合にも役に立つ知識なので覚えておきたいですねー

参考図書

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

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


ゲームプログラマになる前に覚えておきたい技術
平山 尚(株式会社セガ)
秀和システム
売り上げランキング: 10284
 

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

この本はiPhoneでOpenGLを使う方法を解説した本です。2Dゲームの「はえたたき」から3Dゲームの「カーレース」まで、サンプルがとても充実しているので、ゲームを作りはじめようという時に非常に良い取っ掛かりになります!また、パーティクルや衝突判定などゲームには欠かせない話もしっかりと説明されている数少ない日本語の書籍なので、ぜひぜひ読んでおきたいですねー(^^)v

OpenGLで作るiPhone SDKゲームプログラミング
横江 宗太(株式会社パンカク)
インプレスジャパン
売り上げランキング: 28,461


「OpenGLプログラミングガイド 原著第5版」

OpenGLといったらまずはコレ。通称赤本と呼ばれるバイブルです。とりあえず、ちょこちょこと参照することが多いので、手元に置いておきたい(というか重すぎてモバイルは無理)一冊です。OpenGLをやるぞ、という意気込み(もしくは自分へのプレッシャ)で買ってしまう一冊です。コレを買ったら後戻りは出来ません。。。

 

OpenGLプログラミングガイド 原著第5版
OpenGL策定委員会
ピアソンエデュケーション
売り上げランキング: 475,144