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

その2 ポリゴン描画は異常に簡単!


 前章でOpenGLな描画を行うための環境を整えました。幾つかの初期化コードの後ゲームループに入れば、そこに自由にOpenGLなコードを記述できます:

// メッセージ ループ
do{
    if( PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) ){
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    } else {
        wglMakeCurrent(dc, glRC);

        // ここから

            glClearColor(0.0f, 0.5f, 1.0f, 1.0f);
            glClear(GL_COLOR_BUFFER_BIT);

            glRectf(-0.5f, -0.5f, 0.5f, 0.5f);

        // この間

        glFlush();
        SwapBuffers(dc);
        wglMakeCurrent(NULL, NULL);

    }
}while( msg.message != WM_QUIT );

これで実験が色々できるわけです(^-^)。

 ゲームなのでポリゴンが出ないと始まりません。という事でOpenGLでポリゴンを出す方法を…と言いたい所なんですが、世の中にOpenGLでポリゴンを出す情報はすでに山ほどあります。車輪の仕組みをもう一度解説するのもまぁどうかと思うわけです。そこでちょっと一ひねり。ポリゴンを出しつつ、DirectXでも同じコードを共有して出せるようにしてみましょう。膨大なゲーム制作にとって、一度作ったゲームのコードやリソースの資産はなるべく残るようにしないとイカンのです。そのためには、OpenGLを始めた今こそまさに「マルチプラットフォームなコード生成」の実験ができるチャンスなわけです!


@ ポリゴンを出すだけなら本当に簡単

 ではOpenGLでポリゴンを出してみましょう。と言っても、これ、本当に簡単なんですよねぇ…。ポリゴンは頂点の連結で形成されますので、頂点情報をOpenGLに教えてあげればいいんです。それにはglVertex関数群を用います。3Dの空間に置くにはglVertex3f関数を用います:

// メッセージ ループ
do{
    if( PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) ){
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    } else {
        wglMakeCurrent(dc, glRC);

        // ここから

            glBegin(GL_TRIANGLE_STRIP);
            glVertex3f(-0.5f, -0.5f, 0.0f);
            glVertex3f(-0.5f, 0.5f, 0.0f);
            glVertex3f( 0.5f, -0.5f, 0.0f);
            glVertex3f( 0.5f, 0.3f, 0.0f);
            glEnd();

        // この間

        glFlush();
        SwapBuffers(dc);
        wglMakeCurrent(NULL, NULL);
    }
}while( msg.message != WM_QUIT );

ポイントは一つ、頂点を登録する時には必ずglBegin関数とglEnd関数の間で記述する事だけです。glBegin関数の引数はプリミティブタイプで、例えばGL_TRIANGLE_STRIP(三角形ストリップ)やGL_TRIANGLE(三角形リスト)など各種あります。OpenGLで特徴なのは「多角形ポリゴン」を表示出来る事です。これはDirectXにはありません。この辺りはプラットフォーム依存性に関わってきそうです。

 上のコードを実行すると、こんな事項結果が得られます:

超簡単〜〜。いいなぁOpenGL(^-^)。

 ポリゴンに色を付けるには頂点カラーを設定します。頂点カラーはglColor関数群で定義します。いわゆるRGBAの32bitカラーの場合はglColor4ub関数です。先のコードの各頂点に色を付けてみます:

// メッセージ ループ
do{
    if( PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) ){
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    } else {
        wglMakeCurrent(dc, glRC);

        // ここから

            glBegin(GL_TRIANGLE_STRIP);

            glColor4f(0xE0, 0xE0, 0x60, 0xff);  // 適当な黄色

            glVertex4f(-0.5f, -0.5f, 0.0f, 1.0f);
            glVertex4f(-0.5f, 0.5f, 0.0f, 1.0f);
            glVertex4f( 0.5f, -0.5f, 0.0f, 1.0f);
            glVertex4f( 0.5f, 0.3f, 0.0f, 1.0f);
            glEnd();

        // この間

        glFlush();
        SwapBuffers(dc);
        wglMakeCurrent(NULL, NULL);
    }
}while( msg.message != WM_QUIT );

この1行でこんな感じに色が付きます:

超〜〜〜簡単!いいねぇOpenGL。

 glColor関数群は現在の頂点カラーを変更する関数なので、色を変更した後は同じ色が適用され続けます。ですから各頂点ごとに色を付けるには頂点座標をglVertex関数群の「前」に設定変更しなければなりません。

 後は頂点情報をどこからか引っ張って来て、描画系関数にどどっと入れていけばメッシュも描画できそうです。



A DirectXとOpenGLで同じコードを使うために

 OpenGLでのポリゴン描画のお話はこれで殆ど終わりです。ん〜あまりにも短い(笑)。そこで、今後の事を鑑みてこれをマルチライブラリ化してみる事にしました。
 
 この描画結果をDirectXでも同じように得るにはどうしたら良いでしょうか?OpenGLとDirectX、双方の機構は全然異なりますが、上のコードの例えば頂点の情報にはかなりの互換性があります。よって頂点情報からポリゴンを描画するには、

リソース設定 struct Vertex {
    float x, y, z;
    union {
       unsigned color;
        struct { unsigned char b, g, r, a; };  // ※リトルエンディアン
    };
};

Vertex vtx[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.5f, 0.0f, 0xffe0e060},
};
描画 // DirectX
virtual void render(
    Vertex *vtx,
    unsigned vtxNum,
    unsigned primNum)
{
    pDev->SetFVF(
        D3DFVF_XYZ | D3DFVF_DIFFUSE
    );
    pDev->DrawPrimitiveUP(
        D3DPT_TRIANGLESTRIP,
        primNum, vtx, sizeof(Vertex)
    );
}
// OpenGL
virtual void render(
    Vertex *vtx,
    unsigned vtxNum,
    unsigned primNum)
{
    glBegin(GL_TRIANGLE_STRIP);
    for (unsigned i = 0; i < vtxNum; i++) {
        glColor4ub(
            vtx[i].r,
            vtx[i].g,
            vtx[i].b,
            vtx[i].a
        );
        glVertex3f(
            vtx[i].x,
            vtx[i].y,
            vtx[i].z
        );
    }
    glEnd();
}

こんな感じで頂点作成部分は共通化できて、それを双方のレンダラに流しこんで描画する事ができるわけです。仮に双方のレンダラクラスを作ったとすると、インターフェイスが一致するわけです:

Renderer基本クラス
class Renderer {
public:
    // 頂点バッファを描画
    virtual void render(Vtx *vtx, unsigned vtxNum, unsigned primNum) = 0;
};

後はDirectXとOpenGLとでこのクラスを継承し、DirectXRendererクラス及びOpenGLRendererクラスを作って実装すれば良いですね。

 前章のお話ですが、main関数が始まってからウィンドウ作成部分も共通化できますね。共通資産はクラス化して大事に使い回すのです!

GameWindowWinクラス
class GameWindowWin {
    HWND hWnd;
    HDC dc;

public:
    // ウィンドウ作成
    bool create(HINSTANCE hInstance, const char* wndName, WNDPROC wndProc) {
       WNDCLASSEX wcex = {
           sizeof(WNDCLASSEX), CS_OWNDC | CS_HREDRAW | CS_VREDRAW, wndProc,
           0, 0, hInstance, NULL, NULL, (HBRUSH)(COLOR_WINDOW+1),
           NULL, wndName, NULL
       };
       if( !RegisterClassEx(&wcex) )
           return false;

       RECT R = { 0, 0, 640, 480 };
       AdjustWindowRect( &R, WS_OVERLAPPEDWINDOW, FALSE );
       hWnd = CreateWindow( wndName, wndName, WS_OVERLAPPEDWINDOW,
                            CW_USEDEFAULT, 0, R.right - R.left, R.bottom - R.top,
                            NULL, NULL, hInstance, NULL );
       if (hWnd == NULL)
           return false;

        // OpenGLで使うデバイスコンテキストを取っておく
        dc = GetDC(hWnd);

        return true;
    }

    // 後処理
    void terminate() {
        if (dc)
            ReleaseDC(hWnd, dc);
    }

    // デバイスコンテキスト貸出
    HDC getDC() {
        return dc;
    }

    // ウィンドウ表示
    void show() {
        ShowWindow(hWnd, SW_SHOW);
    }
};

あ、ウィンドウサイズなどの柔軟性は今は考えていませんよ(^-^;。

 その後に来るOpenGLの初期化は反対に思いっきりライブラリ依存な部分です。DirectXも描画デバイスが必要ですが、それも完全にそのライブラリ依存です。当たり前ですね。これは先のDirectXRendererクラス及びOpenGLRendererクラスが担うのが良さそうです。だって、双方がその描画デバイス(DirectX9ならIDirect3DDevice9、OpenGLならHGLRCハンドル)を使っていますものね。Rendererクラスに初期化メソッドが追加されます:

Renderer基本クラス
struct Vtx;   // 取りあえず事前宣言しておきます

class Renderer {
public:
    // 初期化
    virtual bool initialize() = 0;

    // 後処理
    virtual void terminate() = 0;

    // 頂点バッファを描画
    virtual void render(Vtx *vtx, unsigned vtxNum, unsigned primNum) = 0;
};

派生クラスの初期化の中で双方の具体的な描画デバイスの生成を行います。呼び出し元はRenderer::initialize()を盲目的に呼べば良いだけです。ゲームを終了させた時の後処理も同様ですね。レンダラのデストラクタで後処理をするという方法もありますが、呼び出し順番依存の怖さがありますので、明示的に後処理メソッドを使うことにします。

 描画ループ部分も実際はしっかり凝るのですが、今はこのままにしておきます。さて、ではこの状態でOpenGLとDirectXの双方のmain.cppを見てみましょう(コードが通るために各クラスのメソッドは追加しています):

dx9main.cpp openglmain.cpp
#pragma comment(lib, "d3d9.lib")


#include <windows.h>
#include <tchar.h>

#include "gamewindowwin.h"
#include "dx9/dx9renderer.h"
#include "vertex.h"

// ウィンドウプロシージャ
LRESULT CALLBACK wndProc( HWND hWnd, UINT mes, WPARAM wParam, LPARAM lParam ){
    if( mes == WM_DESTROY || mes == WM_CLOSE ) { PostQuitMessage( 0 ); return 0; }
        return DefWindowProc( hWnd, mes, wParam, lParam );
}

int APIENTRY _tWinMain( HINSTANCE hInstance, HINSTANCE, LPTSTR, int)
{
    // アプリケーションの初期化
    GameWindowWin window;
    if (!window.create(hInstance, "DirectXテスト", wndProc))
        return -1;

    // OpenGL初期化
    DX9Renderer renderer(window.getHWND());
    if (!renderer.initialize())
        return -2;

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

    // リソース作成
    Vertex vtx[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},
    };

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

    // 各種後処理
    renderer.terminate();
    window.terminate();

    return 0;
}

#pragma comment(lib, "OpenGL32.lib")
#pragma comment(lib, "glew32.lib")

#include <windows.h>
#include <tchar.h>

#include "gamewindowwin.h"
#include "opengl/openglrenderer.h"
#include "vertex.h"

// ウィンドウプロシージャ
LRESULT CALLBACK wndProc( HWND hWnd, UINT mes, WPARAM wParam, LPARAM lParam ){
    if( mes == WM_DESTROY || mes == WM_CLOSE ) { PostQuitMessage( 0 ); return 0; }
        return DefWindowProc( hWnd, mes, wParam, lParam );
}

int APIENTRY _tWinMain( HINSTANCE hInstance, HINSTANCE, LPTSTR, int)
{
    // アプリケーションの初期化
    GameWindowWin window;
    if (!window.create(hInstance, "OpenGLテスト", wndProc))
        return -1;

    // OpenGL初期化
    OpenGLRenderer renderer(window.getDC());
    if (!renderer.initialize())
        return -2;

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

    // リソース作成
    Vertex vtx[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},
    };

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

    // 各種後処理
    renderer.terminate();
    window.terminate();

    return 0;
}

双方の違う所を赤色で示しました。御覧下さい。ライブラリの読み込み部分(#pragma comment)はプロジェクトのプロパティのライブラリ設定の所に移せるので実質無くなるとして、双方の違いは派生レンダラクラスのインクルード宣言と、実体生成部分だけです。後はぜ〜んぶ同じ(^-^)

 と言う事はですよ。この全く同じ部分は「共通化」できますよね。main関数内で実質違うのは「レンダラオブジェクト」だけですから、これを外から与えてあげればいいんです。そこで、上の殆ど同じ部分はApplicationクラスというアプリケーションクラスにまとめてしまいましょう。

 main関数内は大きく「初期化」「ループ」「後片付け」に分かれています。Application::runメソッドをエントリとして、この順番で各メソッドを呼び出す事にします。またアプリケーションのコンストラクタにはゲームウィンドウとレンダラを渡すようにします。Applicationクラスの実装はこうなりました:

Applicationクラス
#ifndef __APPLICATION_H__
#define __APPLICATION_H__

#include "gamewindowwin.h"
#include "renderer.h"

class Application {
    GameWindowWin &window;
    Renderer &renderer;
    Vertex vtx[4];

protected:
    // 初期化
    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);

        return true;
    }

    // 後処理
    virtual void terminate() {
        renderer.terminate();
        window.terminate();
    }

public:
    Application(GameWindowWin &window, Renderer &renderer) : window(window), renderer(renderer) {}

    // アプリ開始
    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);
                renderer.swap();
                renderer.end();
            }
        }while( msg.message != WM_QUIT );

        // 各種後処理
        terminate();

        return 0;
    }
};

#endif

どうでしょうこのシンプルさ(笑)。そして、双方のメイン関数はさらにこんな感じになってしまいました:

dx9main.cpp openglmain.cpp
#pragma comment(lib, "d3d9.lib")


#include <windows.h>
#include <tchar.h>

#include "application.h"
#include "gamewindowwin.h"
#include "dx9/dx9renderer.h"

// ウィンドウプロシージャ
LRESULT CALLBACK wndProc( HWND hWnd, UINT mes, WPARAM wParam, LPARAM lParam ){
    if( mes == WM_DESTROY || mes == WM_CLOSE ) { PostQuitMessage( 0 ); return 0; }
        return DefWindowProc( hWnd, mes, wParam, lParam );
}

int APIENTRY _tWinMain( HINSTANCE hInstance, HINSTANCE, LPTSTR, int)
{
    // ゲームウィンドウ初期化
    GameWindowWin window;
    if (!window.create(hInstance, "DirectXテスト", wndProc))
        return -1;

    // OpenGLレンダラ初期化
    DX9Renderer renderer(window.getHWND());
    if (!renderer.initialize())
        return -2;

    // アプリスタート
    Application app(window, renderer);
    return app.run();
}

#pragma comment(lib, "OpenGL32.lib")
#pragma comment(lib, "glew32.lib")

#include <windows.h>
#include <tchar.h>

#include "application.h"
#include "gamewindowwin.h"
#include "opengl/openglrenderer.h"

// ウィンドウプロシージャ
LRESULT CALLBACK wndProc( HWND hWnd, UINT mes, WPARAM wParam, LPARAM lParam ){
if( mes == WM_DESTROY || mes == WM_CLOSE ) { PostQuitMessage( 0 ); return 0; }
return DefWindowProc( hWnd, mes, wParam, lParam );
}

int APIENTRY _tWinMain( HINSTANCE hInstance, HINSTANCE, LPTSTR, int)
{
    // ゲームウィンドウ初期化
    GameWindowWin window;
    if (!window.create(hInstance, "OpenGLテスト", wndProc))
        return -1;

    // OpenGLレンダラ初期化
    OpenGLRenderer renderer(window.getDC());
    if (!renderer.initialize())
        return -2;

    // アプリスタート
    Application app(window, renderer);
    return app.run();
}

レンダラが違うだけで、後は全く同じです。この章のサンプルを実行していただくと分かりますが、本当にこれでDirectX9とOpenGL双方で同じ画面が出ます。


 この章はOpenGLでポリゴンを出す方法が主題ではありますが、DirectX9とOpenGLとでリソースを共有する方法がメインになってしまいました(^-^;。マルチプラットフォームを考える時にこういう「共有化」と「独立部分の洗い出し」が必要になります。リファクタリングを繰り返すと、上のようにどんどんシンプルになっていきます。これが、楽しいのですよ(^-^)