◆◆ SFML で OpenGL を使う ◆◆
SFML2.5 非公式日本語翻訳
今回のテーマは OpenGL そのもの、ではなく、SFML を、OpenGL の実行環境として使う方法について、です。
両者を合わせて使う方法についても触れます。
さて、OpenGL の素敵なところは何かと言えば、いろんな OS で使えるところですよね。
でも OpenGL 単体ではアプリケーションを作れない。
ウィンドウが必要。レンダリングコンテキストが必要。入出力が必要。などなど。
そうなってくると結局、OS に依存したメンドくさいコーディングも必要になってきてしまいます。
だけど、そんな厄介事は SFML のウィンドウモジュールが一網打尽にしてくれます。
さぁ、面倒なことは SFML に任せて、思う存分 OpenGL で踊ろうではありませんか。
ヘッダーとライブラリを設定する
OpenGL のヘッダーファイルは OS によって違ってます。
ですが、SFML を使えばそんなことは気にせずに一発でインクルード完了です。
#include <SFML/OpenGL.hpp>
このヘッダーは、あくまでも OpenGL そのものの関数をインクルードしているだけです。
OpenGL のエクステンションはインクルードしてません。
SFML は 内部で OpenGL のエクステンションもロードしているので、ヘッダーもインクルードしてくれている、と見せかけて、してません。
それはあくまでも内部実装のヒミツなのであります。
ユーザーの立場としては、エクステンションは他の外部ライブラリと同じように、個別に取り扱うことになってます。
ヘッダーファイルをインクルードしたら、次はライブラリをリンクしましょう。
ヘッダーに関しては OS ごとの違いを気にしなくてもよかったのですが、ライブラリの場合は気にしないとイケません
(Windows の場合は "opengl32"、Linux の場合は "GL"、などなど)。
OpenGL の関数名には頭に "gl" というプレフィックスがついてます。
このことを覚えておくと、リンカエラーが出たときにヒントになります。
OpenGL のウィンドウを作る
SFML は内部で OpenGL を使ってるので、特に妙技を凝らさなくても、すでに OpenGL を実行できる状態になってます。
sf::Window window(sf::VideoMode(800, 600), "OpenGL");
// ほら簡単!
glEnable(GL_TEXTURE_2D);
...
丸投げすぎて気持ち悪いって思うキミのために、sf::Windowクラスのコンストラクタの引数で、
内部で使う OpenGL の設定を変更できるようになってます。
引数は sf::ContextSettings 構造体です。この構造体で、下記の項目を定義します。
- depthBits:深度バッファを使うときのピクセルごとのビット数( 0 で無効)
- stencilBits:ステンシルバッファを使うときのピクセルごとのビット数( 0 で無効)
- antialiasinfLevel:マルチサンプリング(アンチエイリアシング)のレベル
- majorVersion と minorVersion:使いたい OpenGL のバージョン
sf::ContextSettings settings;
settings.depthBits = 24;
settings.stencilBits = 8;
settings.antialiasingLevel = 4;
settings.majorVersion = 3;
settings.minorVersion = 0;
sf::Window window(sf::VideoMode(800, 600), "OpenGL", sf::Style::Default, settings);
グラフィックカードがサポートしてない値があれば、SFML はできるだけ近い値に修正します。
たとえば、4x の アンチエイリアシング が高すぎる場合は 2x を試します。それでもダメなら 0 にします。
実際のところどんな値に設定されたのかは、getSettings() 関数で事後に確認できます。
sf::ContextSettings settings = window.getSettings();
std::cout << "depth bits:" << settings.depthBits << std::endl;
std::cout << "stencil bits:" << settings.stencilBits << std::endl;
std::cout << "antialiasing level:" << settings.antialiasingLevel << std::endl;
std::cout << "version:" << settings.majorVersion << "." << settings.minorVersion << std::endl;
SFML では バージョン3.0以上の OpenGL をサポートしてます(グラフィックドライバが対応してれば)。
SFML2.3 からは 3.2+ のプロファイルのコンテキストと、デバッグフラグのオン/オフもサポートされてます。前方互換プロファイルはまだ未対応です。
デフォルトでは、SFML は 前方互換のプロファイルで 3.2+ のコンテキストを作ります。
なぜかというと、SFML はグラフィックモジュールの内部でレガシーな OpenGL の機能を使ってるからです。
OpenGL と SFML のグラフィックモジュールを混ぜて使うときには、コアプロファイルのコンテキストを使わないようにしてください。
グラフィックモジュールが動作しなくなってしまいます。
OS X では、SFML は コアプロファイルだけを使った 3.2+ をサポートしてます。
OpenGL と SFML のグラフィックモジュールを混ぜて使うときには、レガシーなコンテキストを使わなければいけません。つまり OpenGL2.1 になるということです。
SFML で OpenGL を使うサンプル
SFML での OpenGL のプログラムはこんな感じです。
#include <SFML/Window.hpp>
#include <SFML/OpenGL.hpp>
int main()
{
// ウィンドウ生成
sf::Window window (
sf::VideoMode(800, 600),
"OpenGL",
sf::Style::Default,
sf::ContextSettings(32)
);
window.setVerticalSyncEnabled(true);
// ウィンドウをアクティブに。
window.setActive(true);
// ……画像とかをロードしたり、OpenGL を初期設定したり……
// メインループ
bool running = true;
while (running)
{
// イベントハンドリング
sf::Event event;
while (window.pollEvent(event))
{
if (event.type == sf::Event::Closed)
{
// プログラム終了
running = false;
}
else if (event.type == sf::Event::Resized)
{
// ウィンドウのサイズが変更されたとき、ビューポートを設定しなおす
glViewport (0, 0, event.size.width, event.size.height);
}
}
// バッファをクリア
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// ……描画……
// フレーム終了(初期状態でダブルバッファリングするようになっています)
window.display();
}
// ……リソースを解放……
return 0;
}
このサンプルでは、メインループの while の継続条件に windos.isOpen() を使ってませんね?
プログラムが終了するまで、ウィンドウが開いたままになっている必要があるからです。
最後のループと終了処理の時点で、OpenGL コンテキストが有効じゃないといけないので、こういうふうにしてます。
わからないことがあったら、SFML SDK の中に "OpenGL" と "Window" のサンプルがあるので、
ためらわずに見ておいてくださいませ。詳しく書いてあるので、たぶん絶対役に立つと思いますよー。
OpenGL のコンテキストを管理せよ
SFMLで作成されたウィンドウには漏れなく自動的に1つOpenGLコンテキストがついてきます。
OpenGL関数を実行するときは、現時点でアクティブなコンテキスト上で実行されることになります。
なのでつまり、OpenGL関数が呼び出されるときは必ずコンテキストが1つ有効じゃないとイケないということです。
OpenGL関数の実行時にコンテキストが1つも有効になっていない場合は、お望みの結果にはなりません。
なぜかと言うに、そのOpenGL関数が効果を発揮するステートがどこにも存在しないからです。
ウィンドウのコンテキストをアクティブにするには window.setActive() を使います。window.setActive(true) と書いても同じです。
すでにアクティブなコンテキストが存在するときに別のコンテキストをアクティブにすると、
新しく指定したコンテキストがアクティブになる前に、その時点でアクティブだったコンテキストがコッソリと非アクティブになります。
コッソリじゃなく白昼堂々とコンテキストを非アクティブにするには window.setActive(false) です。
これはコンテキストを別のスレッドでアクティブにするときに必要になります(詳しくはあとで説明します)。
とは言うものの一般的には、ひとまとまりのOpenGLの操作をするたびに毎回愚直に非アクティブにしておくのがオススメです。
このアドバイスに従っておくと、ひとまとまりのOpenGL操作が「アクティブ化」から「非アクティブ化」の間に挟まれるので見やすくなります。
このためにRAIIヘルパークラスを書くと楽しいかもしれません。
// ウィンドウのコンテキストをアクティブ化
window.setActive(true);
// OpenGLステートをセットアップ
// フレームバッファをクリア
// ウィンドウに描画する
// ウィンドウのコンテキストを非アクティブ化
window.setActive(false);
SFMLでOpenGLを使っている箇所のデバッグをするときは、まず最初の1歩として、OpenGL関数が呼ばれているときにコンテキストがアクティブになっているかどうかを確認しましょう。
SFMLが人知れずコンテキストをアクティブにしてくれるだとか、ライブラリに呼び出されたときにSFMLがその時点でアクティブなコンテキストを保存しておいてくれるだとか勝手にアテにしないでね。
唯一保証されているのは、その時点のスレッド上でアクティブなコンテキストは window.setActive(true)~window.setActive(false) の間は変化しないということだけです。
とは言えそれもその間にライブラリに別の呼び出しが一切行なわれない場合のみ。
その他のケースでは必ず、コンテキストは変化する可能性があると心得ておいてください。
なので、以前アクティブだったコンテキストが再びアクティブになるということを保証するには、以前アクティブだったコンテキストを明示的に再度アクティブにしましょう。
それと、OpenGL関数の呼び出し時に、ちゃんと狙い通りのコンテキストがアクティブになっているかも確認しましょう。
アクティブなコンテキストはOpenGLの操作の実行環境を提供してくれるだけでなく、
すべての描画コマンドの宛先となるフレームバッファも指定してくれます。
目に見えるフレームバッファを持っていないコンテキストがアクティブになっているときにOpenGLの描画関数を呼び出すと、
その描画コマンドの出力結果は見えないままとなります。
複数のコンテキストにOpenGLの操作を分割すると、ステートの変化もコンテキストにまたがって分散することになります。
その後の描画処理の中に特定のステートがセットされていることをアテにしているものがあった場合、狙い通りの結果にならなくなる可能性があります。
OpenGLのコードを書くときに絶対守った方がいい心がけがあります。
OpenGL関数を呼び出すたびに毎回必ずOpenGLエラーが出力されているかどうかをチェックすることです。
glGetError()関数を使いましょう。
関数呼び出しのたびに毎回エラーをチェックしておくと、どこでエラーが発生したのかを絞り込むことができます。デバッグ効率大幅アップです。
利用できるコンテキストのバージョンや性能に合わせて、
現在のコンテキストで本当に有効な関数だけが呼び出されるように面倒を見てあげる必要があります。
そうしないと GL_INVALID_OPERATION や GL_INVALID_ENUMのエラーが出まくります。
ウィンドウによって作成されたコンテキストの実際のバージョンと性能を確認するには window.getSettings() を使います。
コンテキストを別個に作成していた場合には context.getSettings() です。
作成されたコンテキストがOpenGLコードの実行に必要な機能を本当に持っているかどうかは常にチェックするようにしましょう。
OpenGL Extensionを性能の高いコンテキストにロードして性能の低いコンテキストで使用しようとした場合(またはその逆)に話がややこしくなってしまいます。
複数の OpenGLウィンドウ
OpenGL のウィンドウを1つ扱うのも複数扱うのも、大して変わりありません。
覚えておかなきゃいけないことは少しだけ。
1つ目。
OpenGL の機能はその時点でアクティブなコンテキスト上で行われます(つまりアクティブなウィンドウ上で)。
なので、1つのプログラムで2つのウィンドウに描画したい場合は、
どっちのウィンドウがアクティブなのかを、描画する前に設定してあげる必要があります。
setActive()関数を使いましょう。
// window1 をアクティブにする
window1.setActive(true);
// …… window1 に描画する処理 ……
// window2 をアクティブにする
window2.setActive(true);
// …… window2 に描画する処理 ……
1つのスレッドでアクティブにできるコンテキスト(=ウィンドウ)は1つだけです。
なので、あるウィンドウをアクティブにする前に、
そのときアクティブなウィンドウを非アクティブに設定してあげる
必要はありません。
自動的に非アクティブになってくれます。OpenGL がそういう仕様になってます。
2つ目。
SFML で生成された OpenGL のコンテキストは、リソースを共有します。
たとえば、テクスチャや頂点バッファをあるコンテキストで作成したら、他のコンテキストでも使えます。
ウィンドウを生成しなおすとき、OpenGL のリソースを全部作り直す必要もありません。
ただし、共有可能なリソースであれば、の話です。
共有できないリソースというのは、たとえば頂点リストオブジェクトです。
ウィンドウのない OpenGL
たまに、ウィンドウがなくて、つまり OpenGL コンテキストがなくて、でも OpenGL の関数を使いたい、っていうとき、ありますよね。
たとえば別のスレッドでテクスチャをロードしたときとか、ウィンドウを生成する前とか。
sf::Contextクラスを使うと、ウィンドウレスのコンテキストを作れます。
このクラスのインスタンスを生成すれば、コンテキストを取得できます。
int main()
{
sf::Context context;
// …… OpenGLのリソースをロード……
sf::Window window (sf::VideoMode(800, 600), "OpenGL");
...
return 0;
}
別スレッドでレンダリング
マルチスレッドの使い道でよくあるのが、あるスレッド(メイン)でウィンドウとイベントを管理して、
レンダリングは別のスレッドで行う、というパターンです。
こういうふうにするとき、注意事項が1つ。
あるコンテキスト(ウィンドウ)が、あるスレッドでアクティブになっているとき、
そのコンテキストを別のスレッドでアクティブにすることはできません。
つまり、レンダリング用のスレッドを立ち上げる前に、ウィンドウを非アクティブにしておく必要がある、ということです。
void renderingThread(sf::Window* window)
{
// ウィンドウ(コンテキスト)をアクティブにする
window->setActive(true);
// レンダリングループ
while (window->isOpen())
{
// ……描画……
// 1フレーム終了。display() はレンダリング関数なので、このとき、コンテキストがアクティブである必要がある。
window->display();
}
}
int main()
{
// ウィンドウ生成(OS の仕様上、ウィンドウはメインスレッドで生成しておくのが無難)
sf::Window window (sf::VideoMode(800, 600), "OpenGL");
// ウィンドウ(コンテキスト)を非活性にする
window.setActive (false);
// レンダリングスレッドを起動
sf::Thread thread (&renderingThread, &window);
thread.Launch();
// メインループ
while (window.isOpen())
{
...
}
return 0;
}
OpenGL と SFML のグラフィックスモジュールを一緒に使う
ここまで、OpenGL を SFML のウィンドウモジュールと一緒に使うやり方を解説してきました。わりと簡単でしたよね?
なぜ簡単だったかというと、ウィンドウモジュールはそもそも OpenGL で描画を行うための土台だからです。描画機能はない。
描画機能はグラフィックモジュールの担当です。そしてその内部では OpenGL を使ってます。
なので、お互いに衝突しないように気をつけてあげないといけません。
SFML のグラフィックモジュールについてよく知らないキミは、ここで1つだけ覚えておいてください。
sf::Windowクラスは sf::RenderWindowクラスに置き換え可能です。
sf::RenderWindowクラスは sf::Windowクラスを継承してて、SFML専用の描画機能が追加されたものです。
さて、SFMLとOpenGLが衝突しないようにするには、
両者を切り替えるたびに、状態をセーブ/リストアするしかありません。
- OenGL で描画
- OpenGL の状態をセーブ
- SFML で描画
- OpenGL の状態をリストア
- OpenGL で描画
……
SFML には pushGLStates()関数、popGLStates()関数、というのがあってですね、
これを使うと SFML がキミのためにあれこれ頑張ってくれます。
glDraw...
window.pushGLStates();
window.draw(...);
window.popGLStates();
glDraw...
SFMLさんは、キミがどんな OpenGL コードを書いたか知る由もないので、
毎回、OpenGL の状態全部をセーブ/リストアします。
小規模なプロジェクトならこれでもいいけれども、規模が大きくなると、遅くなってしまいます。
その場合は、マニュアル操作で OpenGL の状態をセーブ/リストアするとよいです。
glPushAttrib/glPopAttrib、 glPushMatrix/glPopMatrix、などの関数があります。
そしてさらに、描画前に SFML の状態もリストアする必要があります。resetGLStates()関数を使いましょう。
glDraw...
glPush...
window.resetGLStates();
window.draw(...);
glPop...
glDraw...
マニュアル操作でセーブ/リストアすれば、必要なものだけ管理できるし、不必要なドライバコールを抑えられます。