おもちゃラボ

Unityで遊びを作ってます

OpenGLでゲームを作る アニメーションをつける

これまで、7回にわたってOpenGLの基本について説明してきました。ここで基礎編は一段落ということで、最後はアニメーションの付け方と、フレームレートのお話をしたいと思います。アニメーションに関しては難しいことは何もなく、毎フレームごとにオブジェクトを描画する位置を更新することで簡単にアニメーションを付けることが出来ます。プログラムを以下に示します。

#include <GL/glfw.h>
#include <cstdlib>
#include <time.h>
#include <stdio.h>
#include <sys/time.h>
#include <unistd.h>

const int g_windowWidth  = 640;
const int g_windowHeight = 480;
float     g_angle        = 0.0f;

void calcFPS()
{
    static float prevTime = 0.0f;
    struct timeval t;
    gettimeofday(&t, NULL);
    float currentTime = t.tv_usec*1.0E-6;
    
    float diff = (currentTime - prevTime);
    if( diff < 0 ) diff = 1.0f + diff;
    printf("fps=%d\n", (int)(0.5f+1.0/diff));
    
    prevTime = currentTime;
}

void render()
{
    static const GLfloat vtx3[] = {
        0.0f, 100.0f,
        0.0f, 0.0f,
        100.0f, 0.0f,
        100.0f, 100.0f,
    };
    
    glVertexPointer(2, GL_FLOAT, 0, vtx3);
    glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
    
    glEnable(GL_MULTISAMPLE);
    glMatrixMode(GL_MODELVIEW);
    glEnableClientState(GL_VERTEX_ARRAY);

    // 回転アニメーション
    glPushMatrix();
        glTranslatef(320.0f, 240.0f, 0.0f);
        glRotatef(g_angle, 0.0f, 0.0f, 1.0f);
        glDrawArrays(GL_QUADS, 0, 4);
    glPopMatrix();

    glDisableClientState(GL_VERTEX_ARRAY);
}

int main()
{
    if( !glfwInit() ){
        return -1;
    }

    // マルチサンプリング
    glfwOpenWindowHint(GLFW_FSAA_SAMPLES,4);
    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)) {
        
        calcFPS();
        
        if( glfwGetMouseButton(GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS ){
            g_angle += 1.0f;
        }
        
        // 画面の初期化
        glClearColor(0.2f, 0.2f, 0.2f, 0.0f);
        glClear(GL_COLOR_BUFFER_BIT);
        
        render();
        
        // バッファの入れ替え
        glfwSwapBuffers();
    }
    
    glfwTerminate();
    
    return 0;
}

アニメーションをつける

このプログラムでは、マウスをクリックしている間、画面上の四角形が回転します。上にも書きましたが、毎フレームごとに画面のクリアと描画位置の更新をすることでアニメーションをつけることが出来ます。ゲームループの中で毎回マウスが押されたかどうかを判定し、押されていた場合には四角形の回転角度をインクリメントします。その回転角度をrender関数の中でglRotatefに渡すことで回転アニメーションを実現しています。

glRotatefの第一引数には回転角度(単位は度)を渡します。第二引数から第四引数はどの回転軸周りに回転させるかを渡してやります。具体的には回転させる軸には1.0を、その他の軸には0.0を渡してやります。今回はz軸周りに回転させるため、第四引数のみ1.0を指定しています。

f:id:nn_hokuson:20140313192346j:plain

ここで、回転と移動の順番について説明しておきたいと思います。移動に関しては順番に気を使う必要はありません。x方向に100進む操作と、y方向に50進む操作を行うとして、たとえ2つの順番が入れ替わったところで、結局は(100, 50)に着きますよねー。ただし、回転が絡むととたんに順番が大切になってきます。
glRotatefは原点に対して回転を行うので、今回のプログラムのように(320, 240)を中心に回転したい場合には、下図左のように、まず原点を中心に回転し、その後(320, 240)に移動する必要があります。逆に、先に(320,240)に移動してから回転すると、下図右のように原点周りに回転してしまい期待した結果が得られません。


f:id:nn_hokuson:20140312234903j:plain


ということで、(320, 240)を中心として回転させたい場合には、glRotatef→glTranslatefという流れなのですが、プログラムを見てみると

glTranslatef(320.0f, 240.0f, 0.0f);
glRotatef(g_angle, 0.0f, 0.0f, 1.0f);

となっており、glTranslatef→glRotatefの順番になっているように見えます。これは行列を積んでいく仕組み上、仕方のない仕様で、やりたい操作と逆順に行列にプッシュしていく必要があります。(このへんが混乱の原因ですねー:<)ということで、今回はプログラム上はglTranslate→glRotatefになりますが、実際の挙動はglRotatef→glTranslatefになります。ややこしいですね。。。

フレームレート

さて、さいごにフレームレートのお話をしたいと思います。フレームレートとは一秒間に何回ゲームループが回るかを示した値で、単位はFPS(Frame per second)で表します。FPSに関しては【OpenGLでゲームを作る】ダブルバッファリングとは - おもちゃラボで少し説明しましたねー。ここでは、どのようにしてフレームレートを計算するのかという話をしたいと思います。

フレームレートの計算は、「1秒に何回ゲームループが回っているか」を直接調べるのではなく、「ゲームループ1周にかかる時間」の逆数を取ることで計算します。式にすると↓のような感じです。

フレームレート = 1.0 ÷ ゲームループ一周にかかる時間

この計算をしているのがcalcFPS関数です。実装としては、まずgettimeofday関数を使って現在時刻を取得し、ミリセカンド以下をcurrentTimeに保存します。このcurrentTimeと前フレームのprevTimeの差分を見ることで1フレームにかかった時間を計測することが出来ます。最後に計測された時間の逆数をとって、四捨五入した値をフレームレートとして表示しています。

時間計測を行う関数としてはtime()やclock()があるのに、なんでマイナなgettimeofday()を使ってるねん!と思うかもしれません。一応理由はあって、time関数は秒単位の計測しか行えないため、ミリ秒単位の時間測定には適していません。また、clock関数では、自分のプロセスの専有時間しか計測してくれません。 下図で説明すると、clock関数で計測した場合、計測時間は実時間で4秒かかっているにもかかわらず、自プロセスが消費した3秒として通知されます。

f:id:nn_hokuson:20140313203832j:plain

ゲーム中でsleep関数などを使って、他プロセスにタスクスイッチした場合、他プロセスが占める時間はclock関数では計測できないので、正確な時間が測れなくなってしまいます。そこで、ミリ秒単位で実時間計測が可能なgettimeofdayを使っているのです。このへんのお話は、時間の計測を行う - 日進月歩で詳しく説明されているのでご参照下さい。

最近のハードウエアではモニタの描画頻度をみて、自動的にスリープする機能があります。なので今回のプログラムだと大体60FPS程度の値が出るのではないでしょうか。フレーム毎にFPSがばらつくのが気になる方は、次のようなローパスフィルタを入れてみると良いかもしれません。

const int SMOOTH_STRENGTH = 16;
float smoothValues[SMOOTH_STRENGTH]={0.0};
float smooth(float val)
{
    float average = val;
    for(int i = 1; i < SMOOTH_STRENGTH; ++i){
        average+=smoothValues[i];
    }
    average /= SMOOTH_STRENGTH;
    
    for(int i = 0; i < SMOOTH_STRENGTH-1; ++i){
        smoothValues[i]=smoothValues[i+1];
    }
    smoothValues[SMOOTH_STRENGTH-1] = average;

    return average;
}

ローパスフィルタのアルゴリズムとしては、過去16フレーム分のデータを保持しておいて、それらの値と今回のデータの平均を取ることで平滑化するというものです。

まとめ

今回は、基礎編の最終回ということでアニメーションの付け方と、フレームレートのお話をしました。また、オブジェクトを動かす際に移動と回転の順序によって描画結果が変わってしまうということを説明しました。全8回でウインドウを表示することから始めて、図形の描画、画像の描画、入力の取得、図形のアニメーションと、ゲームに必要な技術をひととおり説明してきました。次回からは、実際にゲームを作ってみたいと思います。

参考図書

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

この本は「ゲームをどのように作るのか」といった薄っぺらい内容ではなく、ゲームを動かすための技術を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