Dear ImGui で作成したウィンドウの中に OpenGL で描画するサンプルプログラムです。
私は授業の宿題の雛形を GLFW ベースで作っているのですが、これには多少なりとも(授業内容とは関係ない)コード量を減らすために、さらに自前の補助プログラムを追加しいたりします。しかし、この補助プログラムには今まで GUI が全くついていませんでした。それで、先日これに Dear ImGui を組み込めるようにしたのですが、
imguiの中のウィンドウでOpenGLの描画をするようなサンプルはまだ存在していなさそうなので、用意していただけるとありがたいです。
— 土鍋 (@ssaattwworg) December 31, 2019
おそらくリソースを割く価値はあると思います
ということでしたので、去年の大晦日から随分間が空いてしまいましたけど、ちょっとサンプルプログラムを作ってみました。
Omar 氏のアドバイス
I think you only need to render to a texture e.g. https://t.co/hVpNqk9DS7
— Omar (@ocornut) January 11, 2020
Once you have your framebuffer in a texture you can call ImGui::Image or ImDrawList::AddImage() functions.
によれば、ImGui::Image()
なり ImDrawList::AddImage()
に OpenGL などのテクスチャを渡せば描いてくれるそうなので、Dear ImGui のサンプル Image Loading and Displaying Examplesに従って、フレームバッファオブジェクト (Frame Buffer Object, FBO) を使ってみようと思います。
とりあえず FBO のカラーバッファに使う二次元テクスチャを作ります。fboWidth
と fboHeight
は、それぞれ FBO の幅と高さです。
// カラーバッファ用のテクスチャを用意する
GLuint cb;
glGenTextures(1, &cb);
glBindTexture(GL_TEXTURE_2D, cb);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, fboWidth, fboHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, 0);
次に、FBO のデプスバッファに使うレンダーバッファを作ります。
// デプスバッファ用のレンダーバッファを用意する
GLuint rb;
glGenRenderbuffers(1, &rb);
glBindRenderbuffer(GL_RENDERBUFFER, rb);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, fboWidth, fboHeight);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
これらを使って FBO を作ります。
// フレームバッファオブジェクトを作成する
GLuint fb;
glGenFramebuffers(1, &fb);
glBindFramebuffer(GL_FRAMEBUFFER, fb);
// フレームバッファオブジェクトにカラーバッファとしてテクスチャを結合する
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, cb, 0);
// フレームバッファオブジェクトにデプスバッファとしてレンダーバッファを結合する
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rb);
// フレームバッファオブジェクトの結合を解除する
glBindFramebuffer(GL_FRAMEBUFFER, 0);
あとは描画の前に
glBindFramebuffer(GL_FRAMEBUFFER, fb);
を実行すれば、それ以降の描画は画面ではなく FBO に行われるようになります。FBO への描画が終わったら、
glBindFramebuffer(GL_FRAMEBUFFER, 0);
を実行しておきます。この直前に glFlush()
を置いた方がいいかもしれません。
これまでの処理でカラーバッファに使ったテクスチャ cb
に描画結果が入っていますから、これを ImGui::Image()
を使って描画します。例えば、こんなインタフェースを作ったとします。
// ImGui のフレームを準備する
ImGui::NewFrame();
// ImGui のフレームに一つ目の ImGui のウィンドウを作成する
ImGui::Begin("Control panel");
// FPS など表示してみたりする
ImGui::Text("Frame rate: %6.2f fps", ImGui::GetIO().Framerate);
// スライダでオイラー角を設定する
ImGui::SliderAngle("Roll", &roll);
ImGui::SliderAngle("Pitch", &pitch);
ImGui::SliderAngle("Yaw", &yaw);
// Quit ボタンのクリックでループを抜けるようにする
if (ImGui::Button("Quit")) window.setClose();
この後に OpenGL による処理を行います。以下は宿題の補助プログラムを使ってるので、読み飛ばしてください。これはモデル変換行列と投影変換行列を求め、モデル変換行列にビュー変換行列を乗じて、光源のデータとともにシェーダに渡しています。
// モデル変換行列にオイラー角を乗じる
mm = mm.rotateY(yaw).rotateX(pitch).rotateZ(roll);
// フレームバッファオブジェクトのサイズをもとに投影変換行列を設定する
const GgMatrix mp(ggPerspective(0.5f, (GLfloat)fboWidth / (GLfloat)fboHeight, 1.0f, 15.0f));
// シェーダプログラムを指定する
simple.use(mp, mv * mm, lightBuffer);
OpenGL の描画処理(ドローコール)の前に、FBO を指定します。ビューポートは FBO に使ったテクスチャのサイズと一致させます。画面クリアには glClear()
を使っても構わないのですが、気分的にここでは glClearColor()
を使いたくなかったので、glClearBuffer()
で画面 (FBO) を消去します。なお、画面クリアの色のアルファ値を 0 より大きくしないと、背景色が見えません(少し悩みました)。
// フレームバッファオブジェクトを結合する
glBindFramebuffer(GL_FRAMEBUFFER, fb);
// ビューポートをフレームバッファオブジェクトのサイズにする
glViewport(0, 0, fboWidth, fboHeight);
// カラーバッファを消去する (glClear() でも構わない)
constexpr GLfloat color[]{ 0.2f, 0.3f, 0.5f, 0.8f }, depth(1.0f);
glClearBufferfv(GL_COLOR, 0, color);
glClearBufferfv(GL_DEPTH, 0, &depth);
そして、実際に図形を描画します。これは glDrawElements()
を呼び出しています。
// 図形を描画する
object.draw();
図形の描画が終わったら、フレームバッファを元に戻します。また、ビューポートを親ウィンドウのサイズに戻します。
// フレームバッファオブジェクトの結合を解除する
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// ビューポートを復帰する
window.resetViewport();
レンダリング結果のテクスチャが cb
に入っていますから、これを ImGui::Image()
で描画します。この第3、第4引数は描画するテクスチャの範囲です。これら省略してデフォルト引数を使ってしまうと図形の上下が反転してしまうので、左下が原点となるように、第3引数に ImVec2(0, 1)
、第4引数に ImVec2(1, 0)
を指定して、y 方向の範囲を反転します。
// テクスチャを ImGui のウィンドウに描く
ImGui::Image((void*)(intptr_t)cb, ImVec2(fboWidth, fboHeight), ImVec2(0, 1), ImVec2(1, 0));
Dear ImGui のフレームへの描画を完了します。
// ImGui のウィンドウの作成を終了する
ImGui::End();
// ImGui のフレームに描画する
ImGui::Render();
この処理で次のような表示になります。
ウィンドウを分けて描くこともできます。最初のユーザインタフェースを描画したところで、一旦、Dear ImGui のウィンドウの作成を終了し、
// ImGui のフレームに一つ目の ImGui のウィンドウを作成する
ImGui::Begin("Control panel");
// FPS など表示してみたりする
ImGui::Text("Frame rate: %6.2f fps", ImGui::GetIO().Framerate);
// スライダでオイラー角を設定する
ImGui::SliderAngle("Roll", &roll);
ImGui::SliderAngle("Pitch", &pitch);
ImGui::SliderAngle("Yaw", &yaw);
// Quit ボタンのクリックでループを抜けるようにする
if (ImGui::Button("Quit")) window.setClose();
// 【追加】ImGui のウィンドウの作成を終了する
ImGui::End();
ImGui::Image()
を別のウィンドウで実行するようにして、cb
のテクスチャを描画します。
// 【追加】ImGui のフレームに二つの ImGui のウィンドウを作成する
ImGui::Begin("OpenGL");
// テクスチャを ImGui のウィンドウに描く
ImGui::Image((void*)(intptr_t)cb, ImVec2(fboWidth, fboHeight), ImVec2(0, 1), ImVec2(1, 0));
// ImGui のウィンドウの作成を終了する
ImGui::End();
この場合は次のような表示になります。