ホーム < ゲームつくろー! < OpenGL技術編

その3 ワールドビュー射影変換行列


 前章でOpenGLでポリゴンを描画してみました。とってもとっても簡単です。ただ、さすがにこれだけでは不便極まりないわけで、セオリー通りにワールド変換行列、ビュー行列そして射影変換行列を設定して世界を構築する必要があります。



@ OpenGLでの行列変換

 OpenGLには行列系の関数が幾つか用意されています。これらの関数を呼び出すと、OpenGLは内部にストックされている行列にその関数の行列を自動的に掛け算してくれます。例えば平行移動させたいなぁと思ったらglTranslatef関数を呼ぶと今のストック行列に平行移動行列を掛け算してくれます。

 ある頂点を行列変換する時には、現在のストック行列を単位行列でクリアします。それにはglLoadIdentity関数を用います。その後でglTranslatef関数を呼び出すと平行移動行列が単位行列に掛け算されます。

 通常姿勢の情報はモデルが持っています。毎フレーム自分の姿勢を更新し「その結果」をレンダラが反映する事になります。結果の渡し方として、例えば次のような状況だとしたらどうでしょうか?

モデルA
@ 平行移動
A 回転
B スケール変換

モデルB
@ スケール変換
A 回転
B 平行移動

 モデルAとBとで行列の掛ける順番が異なっています。レンダラはこの順番をモデルから貰って対応する関数(glTranslatef、glRotatef、glScalef)をそれぞれ呼び出す事になります。

 ただ…これ、レンダラの仕事がえらい大変になってしまいます。レンダラにしてみたら「行列の掛け算くらい先にやっておいてくれや」と思うはずなんです。もし各モデルが行列演算を先にやっておいてくれていて、レンダラは指定の頂点を動かす「合成ワールド変換行列」だけを受け付けるとなれば、レンダラの仕事は楽に、そして統一されます:

モデルA
@ 平行移動
A 回転
B スケール変換
→ C @〜Bを掛け算した合成行列を作成

モデルB
@ スケール変換
A 回転
B 平行移動
→ C @〜Bを掛け算した合成行列を作成

という事で、各モデルは自分の姿勢行列の計算責任を持つ、レンダラはその姿勢行列を責任持って頂点に適用する。この流れで行きましょう。


 OpenGLで行列を直接指定するにはglLoadMatrixf関数を用います:

glLoadMatrix関数
void glLoadMatrixf (
    const GLfloat *m
);

GLfloatはOpenGLが定義する単精度浮動小数点ですが、Windowsならfloatと実質同じです。mはfloat[16]とfloat型が16個並んだ配列の先頭ポインタを渡します。これはもちろん行列の情報です。で、どういう行列を渡すのかなぁと試してみたら、あら、行オーダー行列で良いではないですか:

行オーダー平行移動行列の例
Float4x4 matrix;

float m[16] = {
    1.0f, 0.0f, 0.0f, 0.0f,
    0.0f, 1.0f, 0.0f, 0.0f,
    0.0f, 0.0f, 1.0f, 0.0f,
    0.2f, 0.4f, 0.0f, 1.0f
};

memcpy(matrix.v, m, sizeof(float) * 16);

このFloat4x4(これは独自の行列用構造体です)なmatrix変数をglLoadMatrixf関数に直接渡し、続いて頂点を設定します:

行オーダー平行移動行列の例
// ワールド行列設定
glLoadIdentity();
glLoadMatrixf(matrix.v);

// 頂点設定
glBegin(GL_TRIANGLE_STRIP);
for (unsigned i = 0; i < vtxNum; i++) {
    const Vertex &v = vtx[i];
    glColor4ub(v.r, v.g, v.b, v.a);
    glVertex3f(v.x, v.y, v.z);
}
glEnd();   // ←この時に行列が頂点にドバーッと反映される!

これだけで頂点座標に行列が適用されて描画位置が変わります。



A 板ポリを行列でずらしてみる(OpenGL、DirectX9両対応)

 それでは、前章のマルチライブラリ対応の続きです。頂点座標に行列変換を適用する部分を追加するのが目標です。まず、行列構造体(Float4x4)を追加します。DirectX9だとD3DXMATRIX構造体がそれですが、DirectXの無い環境で使えませんので独自に作るんです。今の所中身は呆れるほど簡単です:

Float4x4構造体
struct Float4x4 {
    float v[16];
};

(^-^;。

 Render::renderメソッドは現在頂点座標と頂点数、プリミティブ数を引数に取っています:

Render::renderメソッド
virtual void render(Vertex *vtx, unsigned vtxNum, unsigned primNum);

これだとローカル頂点しか描画できないので、この引数に「変換行列」を追加した実装に変えます:

OpenGLRender::renderメソッド
virtual void render(Vertex *vtx, unsigned vtxNum, unsigned primNum, const Float4x4 &matrix) {
    glLoadIdentity();
    glLoadMatrixf(matrix.v);

    glBegin(GL_TRIANGLE_STRIP);
    for (unsigned i = 0; i < vtxNum; i++) {
        const Vertex &v = vtx[i];
        glColor4ub(v.r, v.g, v.b, v.a);
        glVertex3f(v.x, v.y, v.z);
    }
    glEnd();

}

太文字部分を追加しただけです。これで頂点座標が変換されます。

 続いてこのメソッドの呼出部分。現在これを呼び出しているのはApplication::runメソッドのゲームループ内です。ひとまず、Applicationにmatrix変数を一つメンバに持たせて、それを渡すようにしましょう:

Applicationクラスの変更
class Application {
    GameWindowWin &window;
    Renderer &renderer;
    Vertex vtx[4];
    Float4x4 matrix;

    // アプリ開始
    int run() {
        if (!initialize())
            return -1;

        // ウィンドウ表示
        window.show();

        // メッセージ ループ
        MSG msg;
        do{
            if( PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) ){
                TranslateMessage(&msg);
                DispatchMessage(&msg);
            } else {
                renderer.begin();
                renderer.render(vtx, 4, 2, matrix);
                renderer.swap();
                renderer.end();
            }
        }while( msg.message != WM_QUIT );

        // 各種後処理
        terminate();

        return 0;
    }
};

このmatrixに値を与える必要がありますが、それはApplication::initializeメソッド内にしておきましょうか:

Application::intializeメソッドで平行移動行列を作成
class Application {
    GameWindowWin &window;
    Renderer &renderer;
    Vertex vtx[4];
    Float4x4 matrix;

    virtual bool initialize() {
        // リソース作成
        Vertex v[4] = {
            {-0.5f, -0.5f, 0.0f, 0xffe0e060},
            {-0.5f, 0.5f, 0.0f, 0xffe0e060},
            { 0.5f, -0.5f, 0.0f, 0xffe0e060},
            { 0.5f, 0.3f, 0.0f, 0xffe0e060},
        };
        memcpy(vtx, v, sizeof(Vertex) * 4);

        float m[16] = {
            1.0f, 0.0f, 0.0f, 0.0f,
            0.0f, 1.0f, 0.0f, 0.0f,
            0.0f, 0.0f, 1.0f, 0.0f,
            0.2f, 0.4f, 0.0f, 1.0f
        };
        memcpy(matrix.v, m, sizeof(float) * 16);


        return true;
    }
};

X座標に0.2f、Y座標に0.4fだけ平行移動する行オーダーの行列です。さ、これでもう行列変換して位置が移動するはずです。ビルド&実行!

お〜右上にちゃんとズレました〜。

 DirectX9側もDX9Renderer::renderメソッドの引数を変更し、中身を対応するだけです:

DX9Render::renderメソッド
virtual void render(Vertex *vtx, unsigned vtxNum, unsigned primNum, const Float4x4 &matrix) {
    pDev->SetTransform(D3DTS_WORLD, (D3DXMATRIX*)&matrix);
    pDev->SetRenderState(D3DRS_LIGHTING, FALSE);
    pDev->SetFVF(D3DFVF_XYZ | D3DFVF_DIFFUSE);
    pDev->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, primNum, vtx, sizeof(Vertex));
}

太文字の所を追加するだけ。これで上と同じ結果がDirectX9でも出現します。



B そろそろ世界を作ろうか

 さて、ローカル頂点に適用する行列をレンダラに渡すことで自由に頂点座標を変換できるようになりました。Aまではいわゆる「ワールド変換行列」を設定しただけで、世界は(-1)〜(+1)という狭き空間です。しかもスクリーン比率を思いっきり無視してます。世界を切り取り、スクリーンの比率に合わせて歪むこと無く表示するには、「ビュー行列」と「射影変換行列」が絶対に必要です。

 ビュー行列や射影変換行列はモデルがあまり知る必要のない行列です。むしろ世界を構築する人が知るべき行列なので、新しい世界を表すクラスとして「ScreenLayer」というクラスを新設してその人に管理してもらうことにしましょう:

ScreenLayerクラス
#include "oxfloat.h"

class ScreenLayer {
Float4x4 view, proj;    // ビュー行列、射影変換行列

public:
    ScreenLayer() : view(Float4x4::getIdentity()), proj(Float4x4::getIdentity()) {}

    // ビュー行列を設定
    void setView(const Float4x4 &view) {
        this->view = view;
    }

    // ビュー行列を取得
    const Float4x4 &getView() const {
        return view;
    }

    // 射影変換行列を設定
    void setProj(const Float4x4 &proj) {
        this->proj = proj;
    }

    // 射影変換行列を取得
    const Float4x4 &getProj() const {
        return proj;
    }
};

 ScreenLayerクラスはビュー行列と射影変換行列を持っていて、レンダラはそこからそれぞれの行列を取り出して頂点に適用します。よって、Renderer::renderメソッドの引数にレイヤーが追加されます。そして、renderメソッドの中で行列の掛け算がお目見えします:

OpenGLRender::renderメソッド
virtual void render(Vertex *vtx, unsigned vtxNum, unsigned primNum, const Float4x4 &matrix, const ScreenLayer &layer) {
    glLoadIdentity();

    glMultMatrixf(layer.getProj().v);
    glMultMatrixf(layer.getView().v);
    glMultMatrixf(matrix.v);

    glBegin(GL_TRIANGLE_STRIP);
    for (unsigned i = 0; i < vtxNum; i++) {
        const Vertex &v = vtx[i];
        glColor4ub(v.r, v.g, v.b, v.a);
        glVertex3f(v.x, v.y, v.z);
    }
    glEnd();
}

ここ超注意です!順番を御覧下さい。最初にストック行列を単位行列化しています(glLoadIdentity関数)。その次、射影変換行列が掛け算されています。え!これはOpenGLの仕様です。OpenGLはストック行列に対して掛け算を行う時に「左から」掛けて結果を更新します。次にビュー行列を掛けていますが、これも左から掛け算されまます。どんどん左へ左へと掛け算されるんです。そのため、ローカル頂点を動かすワールド変換行列は最後に掛け算されることになります。とっても注意です。

 DirectX9のレンダラ(DX9Renderer)はこれらの行列を素直に設定するだけです:

DX9Render::renderメソッド
virtual void render(Vertex *vtx, unsigned vtxNum, unsigned primNum, const Float4x4 &matrix, , const ScreenLayer &layer) {

    pDev->SetTransform(D3DTS_WORLD, (D3DXMATRIX*)&matrix);
    pDev->SetTransform(D3DTS_VIEW, (D3DXMATRIX*)&layer.getView());
   pDev->SetTransform(D3DTS_PROJECTION, (D3DXMATRIX*)&layer.getProj());
    pDev->SetRenderState(D3DRS_LIGHTING, FALSE);
    pDev->SetFVF(D3DFVF_XYZ | D3DFVF_DIFFUSE);
    pDev->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, primNum, vtx, sizeof(Vertex));
}

 ビュー行列と射影変換行列で世界を構築すると、ポリゴンのスケールもそのサイズに合わせる必要があります。ポリゴンはApplication::initializeメソッドで定義していたのでした。今は超ちっちゃいポリゴンなので、程々に大きくしてあげます。ついでにそこでワールド・ビュー・射影変換行列もここで初期化してしまいましょう:

Application::initializeメソッド
virtual bool initialize() {
    // リソース作成
    Vertex v[4] = {
        {-10.0f, -10.0f, 0.0f, 0xffe0e060},   // 大きくしました
        {-10.0f, 10.0f, 0.0f, 0xffe0e060},
        { 10.0f, -10.0f, 0.0f, 0xffe0e060},
        { 10.0f, 10.0f, 0.0f, 0xffe0e060},
    };
    memcpy(vtx, v, sizeof(Vertex) * 4);

    // ワールド変換行列
    float m[16] = {
        1.0f, 0.0f, 0.0f, 0.0f,
        0.0f, 1.0f, 0.0f, 0.0f,
        0.0f, 0.0f, 1.0f, 0.0f,
        0.2f, 0.4f, 0.0f, 1.0f
    };
    memcpy(matrix.v, m, sizeof(float) * 16);

    // ビュー行列作成
    layer.setView(Util::lookAtLH(Float3(0.0f, 0.0f, 0.0f), Float3(0.0f, 10.0f, -40.0f), Float3(0.0f, 1.0f, 0.0f)));

    // 射影変換行列作成
    layer.setProj(Util::perspLH(Util::toRad(40.0f), 640.0f, 480.0f, 1.0f, 500.0f));

    return true;
}

ビュー行列と射影変換行列は自前ユーティリティで作ってしまっています。こういうのは用意しておくと潰しがききますね。カメラを(0.0f, 10.0f, -40.0f)と引きの上気味で設定、視錐台は開き40度で4:3比率、近平面が1.0fで遠平面が500.0fで空間を切り取ります。

 この設定で描画するとポリゴンはこんな感じになりました:

お〜、ちゃんと正方形なポリゴンがちょっと煽り目に描画されましたね。視錐台なので下側の頂点に遠近感も付いています。世界にポリゴンをおいた瞬間です。わ〜い。


 という事で、世界にポリゴンを配置する事がこれでできるようになりました。しかもOpenGLとDirectXで同じ頂点や変換行列を共有できています。詳しくはサンプルでどうぞ(^-^)