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

その1 ライブラリのインストールとOpenGLの初期化


 では、OpenGLを使ってみます。…って、ど、どうするんだろう(^-^;。とりあえずWebを調べる事にします。

 OpenGLの公式サイトはこちらです。ここの右側カラムにある「Getting started with OpenGL(OpenGLを始めてみよう)」が良さそうです。ここの「Downloading OpenGL(OpenGLのダウンロード)」の冒頭を読むと、「主要なデスクトッププラットフォーム(Linux、MacOS XそしてWindows)にはシステムにすでにOpenGLがあります。でもグラフィックスドライバは最新にしてね。」とあります。ういっす(-_-)/

 そのちょっと下「Writing an OpenGL Application(OpenGLアプリケーションを書く)」には「If you are using C/C++,...(C/C++でさらにWindowsだったら、OpenGl32.libという静的ライブラリをリンクしてね。)」とあります。

 「Initialization」には「OpenGLのプログラミングをする前に、初期化をしなくちゃいけない。OpenGLはプラットフォーム独立なので、初期化の標準的な方法は実は無いんだ。初期化には2つの段階があってね、最初はOpenGL contextというのを作って、次にOpenGLで使うすべての関数群を読み込みます。」とあります。ほう…

 OpenGL Context Creationには「もしOpenGLをC/C++で使うんだったら、window toolkitを使うのが良いかもね。楽できますよ。OpenGLに慣れてきたら、こちらを見て学び始められますよ」とあります。この選択は迷うところですが、これまでの経験上「楽をすると後で痛い目をみる」という経験から、本当に最初の最初からやってみようと思います。

 先に進みます。「Getting Functions」には、「C/C++を使う人は絶対にOpenGL API関数群を得ておかないといけないよ。大体は#includeで良いけど、幾つかのライブラリをリンクしないといけないんだ。でも、それだけじゃOpenGLは動かない」。「プラットフォーム特有のAPIコール用の関数をロードしないといけません。extension loading librariesを用いるとスムーズに出来ますよ」。ん〜、これで一先ずすべき事は終わりと。

 まぁ、やってみてですね。



@ OpenGL contextを作成

 取りあえずVisualStudioを立ち上げます。新規のWin32アプリケーションを空っぽの状態で立ち上げmain.cppを追加します。以下のコードが超最小限のWin32アプリケーションです:

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

int APIENTRY _tWinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow )
{
    return 0;
}

本当にゼロから始めるのが最初はいいんですよ(きっと)。

 先ほどの話から、プロジェクトに「OpenGl32.lib」をリンクする必要があるとの事です。…#pragma commen使ってみるかな:

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

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

int APIENTRY _tWinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow )
{
    return 0;
}

ビルドすると…通りました。へぇ‥。

 次はOpenGL contextを作るとありました。先のガイドページを見てみます。「A Note on Platforms」には「以下はWin32ベースのお話です」とあります。よかった(^-^)。

 「Simple Context Creation(簡単なコンテキスト生成)」にならってやってみます。一番最初に「ウィンドウハンドル(HWND)を作る時にそのウィンドウスタイルにCS_OWNDCが含まれてないとイカン」と書いてあります。CS_OWNDCというのは、一つのウィンドウに一つデバイスコンテキストを割り当てるフラグです。なるほど、ではそのハンドルを含めたウィンドウを作成してみましょう: 

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

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

const char *gName = "OpenGLテスト";

// ウィンドウプロシージャ
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 hPrevInstance, LPTSTR lpCmdLine, int nCmdShow )
{
    // アプリケーションの初期化
    MSG msg; HWND hWnd;
    WNDCLASSEX wcex = { sizeof(WNDCLASSEX), CS_OWNDC | CS_HREDRAW | CS_VREDRAW, WndProc, 0, 0, hInstance, NULL, NULL, (HBRUSH)(COLOR_WINDOW+1), NULL, (TCHAR*)gName, NULL};
    if( !RegisterClassEx(&wcex) )
        return 0;

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

    ShowWindow(hWnd, nCmdShow);

    // メッセージ ループ
    do{
        if( PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) ){
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }while( msg.message != WM_QUIT );

    return 0;
}

この辺は「極短Windowsプログラム」が大いに使えますねぇ。取りあえず、これでウィンドウハンドルも取得できて、ウィンドウも出現します。

 次に「PIXELFORMATDESCRIPTOR」という構造体にピクセルフォーマット情報を書き込みます。PIXELFORMATDESCRIPTOR構造体を調べてみると(MSDN)、Windowsが用意したOpenGL用構造体のようで、ちゃんとwingdi.hにありました。

 構造体の中身はかなりな情報が詰まっています。DirectXでIDirect3DDevice9を作る為にD3DPRESENT_PARAMETER構造体を作りますが、そんな感じです、きっと。取りあえずマニュアルにあるデフォルト値(?)を入れておきます:

PIXELFORMATDESCRIPTOR pfd =
{
    sizeof(PIXELFORMATDESCRIPTOR),
    1,
    PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER, //Flags
    PFD_TYPE_RGBA,                                              //The kind of framebuffer. RGBA or palette.
    32,                                                         //Colordepth of the framebuffer.
    0, 0, 0, 0, 0, 0,
    0,
    0,
    0,
    0, 0, 0, 0,
    24,                                                         //Number of bits for the depthbuffer
    8,                                                          //Number of bits for the stencilbuffer
    0,                                                          //Number of Aux buffers in the framebuffer.
    PFD_MAIN_PLANE,
    0,
    0, 0, 0
};

コメントの部分は意味がわかりますね。フラグも「ウィンドウに描画」とか「OpeGLサポート」「ダブルバッファ使う」という感じでしょう、きっと(^-^;。RGBAの32bitフルカラーなバックバッファ(フレームバッファ)を作って、深度バッファ(depth buffer)は24bit、ステンシルバッファは8bitです。Aux Buffer(補助バッファ)は0…MSDNを見ると「Auxiliary buffers are not supported.」サポートしてない。じゃぁゼロでと。

 続けます。この構造体をChoosePixelForamt関数に放り込みます。この関数は指定のピクセルフォーマットに該当するIDを返してくれます。引数にはデバイスコンテキストと先ほどの構造体を要求します。デバイスコンテキストはGetDC関数で得ておきましょう:

HDC dc = GetDC(hWnd);
int format = ChoosePixelFormat(dc, &pfd);
if (format == 0)
    return 0; // 該当するピクセルフォーマットが無い

関数は成功すると1以上の整数を、失敗すると0を返します。帰ってきた整数は構造体で与えたピクセルフォーマットに対応するIDです。私の環境では「9」が返りました。デバイスコンテキストにこのピクセルフォーマットを設定するためにSetPixelFormat関数を次にコールします:

// OpenGLが使うデバイスコンテキストに指定のピクセルフォーマットをセット
if (!SetPixelFormat(dc, format, &pfd))
    return 0; // DCへフォーマットを設定するのに失敗

これでデバイスコンテキストがOpenGL用になりました。

 これでOpenGL contextが作れます。作るのは至極簡単でwglCreateContextという関数にデバイスコンテキストを与えます。関数が成功したらOpenGLのレンダリングコンテキストハンドル(HGLRC: Handle to GL Rendering Context)が返ります:

// OpenGL contextを作成
HGLRC glRC = wglCreateContext(dc);

// 作成されたコンテキストがカレント(現在使用中のコンテキスト)か?
if (!wglMakeCurrent(dc, glRC))
    return 0; // 何か正しくないみたい…

その後にwglMakeCurrent関数を呼んでいます。これは作成されたコンテキストを現在使用中にするものだそうで、絶対に呼ばなければならないとあります。従いましょう。

 これで、OpenGL contextの作成は終了です。ゲームが終わった後などこのコンテキストを削除するにはwglDeleteContext関数を呼ぶのですが、その前に現在使用中のコンテキストを無効にするためwglMakeCurrent関数にNULLを与える必要があります。終了処理はこんな感じです:

// 後処理
// カレントコンテキストを無効にする
wglMakeCurrent(NULL, NULL);

// カレントコンテキストを削除
wglDeleteContext(glRC);

// デバイスコンテキスト解放
ReleaseDC(hWnd, dc);

ここまでのコンパイルが通るコードはこちら:

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

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

const char *gName = "OpenGLテスト";

// ウィンドウプロシージャ
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 hPrevInstance, LPTSTR lpCmdLine, int nCmdShow )
{
    // アプリケーションの初期化
    MSG msg; HWND hWnd;
    WNDCLASSEX wcex = { sizeof(WNDCLASSEX), CS_OWNDC | CS_HREDRAW | CS_VREDRAW, WndProc, 0, 0, hInstance, NULL, NULL, (HBRUSH)(COLOR_WINDOW+1), NULL, (TCHAR*)gName, NULL};
    if( !RegisterClassEx(&wcex) )
        return 0;

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

    // OpenGL初期化
    // ピクセルフォーマット初期化
    PIXELFORMATDESCRIPTOR pfd =
    {
        sizeof(PIXELFORMATDESCRIPTOR),
        1,
        PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER, //Flags
        PFD_TYPE_RGBA, //The kind of framebuffer. RGBA or palette.
        32, //Colordepth of the framebuffer.
        0, 0, 0, 0, 0, 0,
        0,
        0,
        0,
        0, 0, 0, 0,
        24, //Number of bits for the depthbuffer
        8, //Number of bits for the stencilbuffer
        0, //Number of Aux buffers in the framebuffer.
        PFD_MAIN_PLANE,
        0,
        0, 0, 0
    };
    HDC dc = GetDC(hWnd);
    int format = ChoosePixelFormat(dc, &pfd);
    if (format == 0)
        return 0; // 該当するピクセルフォーマットが無い

    // OpenGLが使うデバイスコンテキストに指定のピクセルフォーマットをセット
    if (!SetPixelFormat(dc, format, &pfd))
        return 0; // DCへフォーマットを設定するのに失敗

    // OpenGL contextを作成
    HGLRC glRC = wglCreateContext(dc);

    // 作成されたコンテキストがカレント(現在使用中のコンテキスト)か?
    if (!wglMakeCurrent(dc, glRC))
        return 0; // 何か正しくないみたい…


    ShowWindow(hWnd, nCmdShow);

    // メッセージ ループ
    do{
        if( PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) ){
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }while( msg.message != WM_QUIT );

    // 後処理
    // カレントコンテキストを無効にする
    wglMakeCurrent(NULL, NULL);

    // カレントコンテキストを削除
    wglDeleteContext(glRC);

    // デバイスコンテキスト解放
    ReleaseDC(hWnd, dc);

    return 0;
}

続いては、OpenGLの関数群をロードするそうです。



A OpenGL Functionsのロード

 OpenGLの関数群のロードに関する記述は公式ページのこちらのリンクにあります。冒頭に「Loading OpenGL FunctionsはOpenGL contextを作った後に行う大切な初期化処理だ」と書かれています。その後の記述がポイント。「You are strongly advised to use an Extension Loading Library ...(Extension Loading Libraryを使う事を強くお勧めするよ)」とあります。ん〜…そ、そうするかな。

 という事で、Extension Loadion Libraryのリンクを辿ります。

 Extension Loading LibraryというのはOpenGLの関数群へのポインタをランタイム時(実行中)にロードしてくれる便利ライブラリで、プラットフォーム間で異なるローディング方法を吸収してくれるそうです。素晴らしい(^-^)

 どうやら「GLEW」と「GL3W」という2つの方法が公開されているようです。GLEWの説明を見るといきなり「GLEW's problem is that ... 」と問題点からお話が始まります。どうもGL 3.2以降でglewInit関数(多分初期化関数)を呼ぶと失敗するとあります。が、ページにある方法だと大丈夫だよともあります:

glewExperimental = TRUE;
GLenum err=glewInit();
if(err!=GLEW_OK)
{
    //Problem: glewInit failed, something is seriously wrong.
    cout<<"glewInit failed, aborting."<<endl;
}

GLEW(The OpenGL Extension Wrangler Library)はこちらからDLできます。WindowsバイナリをDLして、適当な所に解凍します。libフォルダに静的ライブラリがあるのでリンク、またinclude\GLにglew.hがありますので、それをインクルードします。実行時にはbinにあるglew32.dllが必要になるため、プロジェクトと同じフォルダに今は置いておきましょう。

 これらを入れこんで実行すると…ん〜何かが変わったに違いない(^-^;。きっと良い事が裏で起きています。見た目にはわかりません。

 こ、これでOpenGLを使えるようになったのでしょうか。何か試す事ができるミニマムコードを探してみます。



B おー動いたぁ(≧▽≦)/

 Web先にある先人の皆様のお力を頂き、ミニマムOpenGLコードが出来ました。次の通りです:

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

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

const char *gName = "OpenGLテスト";

// ウィンドウプロシージャ
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 hPrevInstance, LPTSTR lpCmdLine, int nCmdShow )
{
    // アプリケーションの初期化
    MSG msg; HWND hWnd;
    WNDCLASSEX wcex = { sizeof(WNDCLASSEX), CS_OWNDC | CS_HREDRAW | CS_VREDRAW, WndProc, 0, 0, hInstance, NULL, NULL, (HBRUSH)(COLOR_WINDOW+1), NULL, (TCHAR*)gName, NULL};
    if( !RegisterClassEx(&wcex) )
        return 0;

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

    // OpenGL初期化
    // ピクセルフォーマット初期化
    PIXELFORMATDESCRIPTOR pfd =
    {
        sizeof(PIXELFORMATDESCRIPTOR),
        1,
        PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER, //Flags
        PFD_TYPE_RGBA, //The kind of framebuffer. RGBA or palette.
        32, //Colordepth of the framebuffer.
        0, 0, 0, 0, 0, 0,
        0,
        0,
        0,
        0, 0, 0, 0,
        24, //Number of bits for the depthbuffer
        8, //Number of bits for the stencilbuffer
        0, //Number of Aux buffers in the framebuffer.
        PFD_MAIN_PLANE,
        0,
        0, 0, 0
    };
    HDC dc = GetDC(hWnd);
    int format = ChoosePixelFormat(dc, &pfd);
    if (format == 0)
        return 0; // 該当するピクセルフォーマットが無い

    // OpenGLが使うデバイスコンテキストに指定のピクセルフォーマットをセット
    if (!SetPixelFormat(dc, format, &pfd))
        return 0; // DCへフォーマットを設定するのに失敗

    // OpenGL contextを作成
    HGLRC glRC = wglCreateContext(dc);

    // 作成されたコンテキストがカレント(現在使用中のコンテキスト)か?
    if (!wglMakeCurrent(dc, glRC))
        return 0; // 何か正しくないみたい…


    ShowWindow(hWnd, nCmdShow);

    // メッセージ ループ
    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 );

    // 後処理
    // カレントコンテキストを無効にする
    wglMakeCurrent(NULL, NULL);

    // カレントコンテキストを削除
    wglDeleteContext(glRC);

    // デバイスコンテキスト解放
    ReleaseDC(hWnd, dc);

    return 0;
}

太文字部分がOpenGLのコードです。上から、まずwglMakeCurrent関数でOpenGLのコンテキストを指定します。イメージとしては描画してくれる人が待機していて、その人を指名している状態です。以後、描画関係の呼び出しをするとこの人が処理してくれるというわけです。

 次にglClearColor関数でバックバッファをクリアする時の色を設定しています。引数はRGBAの順です。続くglClear関数で実際にバックバッファを指定カラーで塗りつぶしています。GL_COLOR_BUFFER_BITは色だけクリアするフラグです。ここには他にもGL_DEPTH_BUFFER_BIT(深度バッファ)、GL_ACCUM_BUFFER_BIT(蓄積バッファ)、GL_STENCIL_BUFFER_BIT(ステンシルバッファ)をorでつなげて指定できます。

 バックバッファがクリアされた所で、上の例ではglRectf関数を用いて長方形を一つ描いています。関数の最後の"f"ですが、これは「float(単精度)」という意味で、同じような関数名でもglRectd(double精度)、glRecti(int精度)、glRects(short精度)などがあるようです。なるほどね〜。引数の数字は左上座標(-0.5f, -0.5f)と右下座標(0.5f, 0.5f)をワールド座標で指定します。今はなーんにも座標変換を設定していないので、すべて単位行列、つまり(-1, -1, -1)〜(1, 1, 1)という範囲(スクリーン比率)になっていると思います。なので、画面の真ん中に長方形が出るはずです。

 glFlush関数は、バックバッファへの描画などを実際に行う関数です。OpenGLは描画系関数を呼び出しても直ぐには描画せず「描画キュー」にやるべき事をガンガン溜めていきます。それをぶはー!っと吐き出して一気に描画するのがglFlush関数です。ですから、この関数を呼び出し忘れるとえらい事になるわけです。

 SwapBuffers関数はOpenGLの関数ではなくてWindows APIです。これはデバイスコンテキストをスワップします。もちろん、デバイスコンテキストがその能力を秘めていないといけないのですが、それをOpenGL contextを作る時に設定しました。

 最後にwglMakeCurrent関数にNULLを設定して、描画してくれる人を解放してあげます。

 これで実行すると、わ〜い、絵が描けました:

これでOpenGLの基盤のような物が出来たように思います。まだきっと細かな所で色々と穴はあるんだろうと思いますが、一先ず描画出来たことですし、ここからDirectXと同じように色々発展させていきましょう〜。