OpenGL, Vulkanを使ったアプリケーションで様々なレンダリング手法を実装した例を複数用意したい時
OpenGLやVulkanなどで遊んでいると様々なレンダリング手法を実装してテストしてみたくなります。こういう時に各テストをどのように実装したら簡潔に書くことが出来るのか悩んだので、ここにその解決策の1つを書いておきます。
前提
ディレクトリ構造
example
ディレクトリ以下に各exampleを実装した.hpp
, .cpp
ファイルを配置するようにします。
example/
├── example1
│ ├── shaders
│ │ ├── example1.frag
│ │ └── example1.vert
│ └── src
│ └── example1.cpp
├── common
│ └── src
│ ├── example-base.cpp
│ └── example-base.hpp
├── example2
│ ├── shaders
│ │ ├── example2.frag
│ │ └── example2.vert
│ └── src
│ └── example2.cpp
各exampleで使う共通の処理などはcommon
以下に配置することにします。
exampleの実装
各exampleでは以下のような共通する処理が現れます。
- GLFW, glad, ImGuiのセットアップ
- 実装した自作レンダラーのセットアップ
- シーンの読み込み
- ユーザーからの入力処理
- レンダリングループ
これらの処理は各exampleで完全に一致している部分もあれば、各exampleによって処理内容を変えたい部分もあります。例えばGLFW, glad, ImGui, レンダラーのセットアップなどは共通化できますが、シーンやユーザーからの入力処理、レンダリングループ内での処理内容は各exampleによって変えたい場合があります。
このような時に各exampleをどのように実装したら簡潔かつ拡張性が高くなるように出来るでしょうか?
解決策
各exampleで共通する処理をExampleBase
クラスに実装します。
example-base.hpp
class ExampleBase
{
public:
ExampleBase(uint32_t width, uint32_t height);
virtual ~ExampleBase();
// アプリケーションの起動
void run();
protected:
uint32_t width; // レンダリング画面の横幅
uint32_t height; // レンダリング画面の縦幅
GLFWwindow* window; // GLFWのwindowハンドラ
ImGuiIO* io; // ImGuiのユーザー入力のハンドラ
Camera camera; // カメラを表現するクラス
Scene scene; // シーンを表現するクラス
private:
// GLFWを初期化する
void initGlfw();
// gladを初期化する
void initGlad();
// ImGuiを初期化する
void initImGui();
// レンダリングループ前に行う処理を記述する(例: シーンの読み込み)
virtual void beforeRender() = 0;
// レンダリングループ内におけるImGui関連の処理を記述する(例: ImGui::InputFloat)
virtual void runImGui() = 0;
// ユーザー入力の処理を記述する(例: キー入力でカメラを動かす)
virtual void handleInput() = 0;
// レンダリング処理を記述する(例: Uniform変数のセット, ドローコールの呼び出し)
virtual void render() = 0;
// アプリを終了する前に行う必要があるリソース解放処理を記述する
virtual void release();
// GLFWのframebufferSizeCallbackの処理を記述する
virtual void framebufferSizeCallback(GLFWwindow* window, int width,
int height);
// GLFWのCallback関数用のラッパー
static void framebufferSizeCallbackStatic(GLFWwindow* window, int width,
int height);
};
example-base.cpp
ExampleBase::ExampleBase(uint32_t width, uint32_t height)
: width{width}, height{height}
{
initGlfw();
initGlad();
initImGui();
}
ExampleBase::~ExampleBase() { release(); }
void ExampleBase::initGlfw()
{
if (!glfwInit()) { throw std::runtime_error("failed to initialize GLFW"); }
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // required for Mac
window = glfwCreateWindow(width, height, "example", nullptr, nullptr);
if (!window) { throw std::runtime_error("failed to create window"); }
glfwMakeContextCurrent(window);
glfwSetWindowUserPointer(window, this);
// この関数の引数はstaticメソッドである必要があるため、staticなラッパー関数を代わりに使う
glfwSetFramebufferSizeCallback(window, framebufferSizeCallbackStatic);
}
void ExampleBase::initGlad()
{
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
throw std::runtime_error("failed to initialize glad");
}
}
void ExampleBase::initImGui()
{
IMGUI_CHECKVERSION();
ImGui::CreateContext();
io = &ImGui::GetIO();
ImGui::StyleColorsDark();
ImGui_ImplGlfw_InitForOpenGL(window, true);
ImGui_ImplOpenGL3_Init("#version 460 core");
}
void ExampleBase::framebufferSizeCallback(GLFWwindow *window, int width,
int height)
{
this->width = width;
this->height = height;
glViewport(0, 0, width, height);
}
void ExampleBase::framebufferSizeCallbackStatic(GLFWwindow *window, int width,
int height)
{
ExampleBase *instance =
static_cast<ExampleBase *>(glfwGetWindowUserPointer(window));
instance->framebufferSizeCallback(window, width, height);
}
void ExampleBase::release()
{
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext();
glfwDestroyWindow(window);
glfwTerminate();
}
void ExampleBase::run()
{
beforeRender();
// レンダリングループ
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
runImGui();
handleInput();
render();
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
glfwSwapBuffers(window);
}
}
ExampleBase::run()
を呼び出すことでアプリケーションが起動します。
各exampleではこのクラスを継承し、virtual
になっているメソッドをoverride
していきます。例えば三角形を表示する例は以下のように実装できます。
triangle.cpp
class TriangleExample : public ExampleBase
{
public:
TriangleExample(uint32_t width, uint32_t height) : ExampleBase(width, height) {}
private:
void beforeRender() override
{
// 頂点データ
float vertices[] = {
-0.5f, -0.5f, 0.0f, // position1
1.0f, 0.0f, 0.0f, // color1
0.5f, -0.5f, 0.0f, // position2
0.0f, 1.0f, 0.0f, // color2
0.0f, 0.5f, 0.0f, // position3
0.0f, 0.0f, 1.0f // color3
};
// VBOにデータをセット
buffer.setData(vertices, 18, GL_STATIC_DRAW);
// VAOのセットアップ
vao.bindVertexBuffer(buffer, 0, 0, 6 * sizeof(float));
vao.activateVertexAttribution(0, 0, 3, GL_FLOAT, 0);
vao.activateVertexAttribution(0, 1, 3, GL_FLOAT, 3 * sizeof(float));
// シェーダーのセットアップ
pipeline.loadVertexShader(
std::filesystem::path(CMAKE_CURRENT_SOURCE_DIR) /
"shaders/shader.vert");
pipeline.loadFragmentShader(
std::filesystem::path(CMAKE_CURRENT_SOURCE_DIR) /
"shaders/shader.frag");
}
void runImGui() override {}
void handleInput() override
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
glfwSetWindowShouldClose(window, GLFW_TRUE);
}
}
void render() override
{
glClear(GL_COLOR_BUFFER_BIT);
pipeline.activate();
vao.activate();
glDrawArrays(GL_TRIANGLES, 0, 3);
vao.deactivate();
pipeline.deactivate();
}
Buffer buffer; // Bufferのラッパークラス
VertexArrayObject vao; // VAOのラッパークラス
Pipeline pipeline; // シェーダーのラッパークラス
};
int main()
{
TriangleExample app(1280, 720);
app.run();
return 0;
}
これはいわゆるテンプレートメソッドパターンです。共通する処理をExampleBase
にまとめ、必要な部分だけを各サブクラスで実装することでexampleをミニマルにすることが出来ます。
この例ではImGuiを使っていませんが、runImGui()
にImGui関連の処理を記述することで各exampleでGUIを変えることが出来ます。また、各exampleに固有のパラメーターなどをメンバ変数として持たせ、ImGui経由でそのパラメーターを変化させるといったことも出来ます。
完全な実装例は https://github.com/yumcyaWiz/opengl-sandbox で見ることが出来ます。
この記事ではOpenGLを例に説明しましたが、Vulkanでも同様のことが出来ると思います。
Discussion