- 3か月にわたり Vulkanを学習し、2つのデモゲームを含む小規模ゲームエンジン を自ら実装した経験のまとめ
- 既存の OpenGLの経験を土台に、Vulkanの複雑さを段階的に克服 し、glTFローディング・スキニング・シャドウマッピングなどの中核機能を実装
- エンジンは EDBR(Elias Daler’s Bikeshed Engine) で、約1.9万行のコードで構成されており、バインドレスディスクリプタ・PVP・BDA など現代的なグラフィックス手法を活用
- 記事では vk-bootstrap, VMA, volk のような必須ライブラリや、パイプラインパターン、シェーダービルドの自動化、同期管理 など実務的な実装詳細を共有
- Vulkanへの移行によって グローバル状態の排除、明示的制御、改善されたデバッグ環境、GPU間の一貫性 を得ており、今後は レンダーグラフ・SDFフォント・ボリューメトリック効果 を追加する計画
Vulkan学習とエンジン開発の概要
- 著者はグラフィックスプログラミングを独学で始め、1年半前にOpenGLで3Dエンジンを書いた経験がある
- Vulkanベースのエンジンは 小規模なレベルベースのゲーム に適しており、効率性よりも学習と実験を主目的としている
- 初期段階では単純な3Dゲームを作り、再利用可能な部分を切り出してエンジン化する形で進めた
- 3か月で完成できた理由は、汎用エンジンではなく特定用途のエンジン に限定したため
グラフィックスプログラミングの学習ルート
- 初心者は OpenGL から始めて、テクスチャモデルの表示、Blinn-Phongライティング、シャドウマッピングなどを学ぶのが推奨される
- 推奨資料として learnopengl.com, Anton’s OpenGL 4 Tutorials, Thorsten Thormählenの講義 などが挙げられている
- 最新のOpenGL 4.6実習資料とあわせて、線形代数学(ベクトル、行列、クォータニオン) 理解の重要性を強調
Bike-sheddingを防ぐ助言
- 不要な 過剰設計や抽象化 を避け、「今必要なものだけを実装する」という原則を保つ
- 「まず動くように作り、あとで改善する」というアプローチを推奨
- 汎用目的エンジン よりも 小さなゲーム を先に完成させる方が効率的
- 他人の複雑なコードや構造をそのまま模倣せず、単純な構造から出発する
Vulkanを選んだ理由
- AAAゲームではDirectX、macOS/iOSではMetal、WebではWebGPU/WebGLが主に使われている
- 著者は オープンソースと標準技術を好み、デスクトップ向けの小規模3Dゲーム開発という目的に合わせてVulkanを選択
- OpenGLはもはや発展がなく、macOSでは廃止されている
- WebGPUは簡潔だが、安定性不足、機能制約、バインドレスやプッシュ定数の未サポート などの限界がある
Vulkan学習プロセス
- 初期には「グラフィックスドライバを直接書いているようなもの」と感じるほど難しかったが、
動的レンダリング、vk-bootstrap, vkguide などの登場で取り組みやすさが向上した
- 主な学習資料:
- vkguide.dev(基礎から実践中心)
- TU Wien Vulkan Lecture Series
- 3D Graphics Rendering Cookbook, Mastering Graphics Programming with Vulkan
- 最初の1か月でglTFローディング、コンピュートスキニング、フラスタムカリング、シャドウマッピングの実装を完了
EDBRエンジン構造とフレーム処理
- エンジンコードは約 19,000行、3Dゲームが4,600行、2Dプラットフォームゲームが1,200行
- 主なレンダリング段階:
- Computeスキニング → Cascaded Shadow Mapping(4096×4096) → PBRベースのジオメトリシェーディング
- Depth Resolve → Post FX(深度ベースのフォグ) → UIレンダリング
- すべてのグラフィックスシステムは Vulkan専用に書き直されており、以前のOpenGLコードと混在させていない
Vulkan開発の実務的なヒント
推奨ライブラリ
- vk-bootstrap: 初期化とスワップチェーン設定を簡略化
- Vulkan Memory Allocator(VMA): メモリ管理を自動化
- volk: 拡張関数のロードを簡略化
GfxDevice抽象化
VkDevice, VkQueue, VmaAllocator などを1つのオブジェクトで管理
- フレーム開始/終了、イメージ・バッファ生成、バインドレスディスクリプタ管理を担当
シェーダー管理
- GLSLを使用 し、ビルド時に
glslc でSPIR-Vへ事前コンパイル
- CMake
DEPFILE を利用して、シェーダー変更時に自動で再ビルド
パイプラインパターン
- 各レンダリング段階を クラス単位のパイプライン に分離(
init, cleanup, draw)
VK_KHR_dynamic_rendering を使うことで レンダーパス・サブパスを省略 し、単純化した構造を維持
Programmable Vertex Pulling + Buffer Device Address
- 1つの Vertex構造体 ですべてのメッシュを処理
- シェーダー内で buffer_reference によって直接アクセスし、VAOを不要にする
Bindless Descriptor
- グローバルなテクスチャ配列(
textures[], samplers[])を使って テクスチャIDベースでサンプリング
- マテリアル構造体にテクスチャIDを保持し、プッシュ定数で渡す
動的データアップロード
- フレームごとにGPUバッファを差し替えるか、CPUステージングバッファを用いてデータ転送
NBuffer クラスでフレームインフライト構造を管理
リソース整理と同期
- 明示的なcleanup関数 を使い、デストラクタによる自動解放ではなく手動で管理
vkCmdPipelineBarrier2 で パス間のメモリ同期 を実行
- Render Graphは今後実装予定
実装の詳細事例
スプライトレンダリング
- バインドレステクスチャ と インスタンシング によって数千個のスプライトを一度にレンダリング
SpriteDrawCommand 構造体をGPUバッファにアップロードし、vkCmdDraw(6, N) を呼び出す
- 1万個のスプライトを 315μs でレンダリング
Computeスキニング
- コンピュートシェーダーで ボーン行列とウェイトに基づく頂点変形 を実行
- 各インスタンスごとに出力バッファを別途生成し、その後のレンダリング段階では同様に処理
ゲーム/レンダラー分離
- ゲームロジックは entt ECS を使用し、レンダラーは DrawCommandベクタ だけを処理
drawMesh, drawSkinnedMesh 呼び出しでレンダーコマンドを生成
シーンローディングとプレハブ
- BlenderからglTFでレベルを構成 し、ノード名の規則でプレハブを自動スポーン
- プレハブはJSONで定義し、外部glTFを参照
MSAA、UI、ImGui
- ForwardレンダリングベースのMSAA x8 を適用
- Roblox UI API に着想を得た自動レイアウトシステムを実装
- Dear ImGuiのsRGB問題 を解決するため、独自のVulkanバックエンドを作成
その他の構成要素
- Jolt Physics で物理を処理し、entt ECS、OpenAL-soft オーディオ、Tracy プロファイラを使用
Vulkan移行の利点
- グローバル状態の排除 により、明示的でモジュール化されたコード構造を実現
- 検証レイヤーとRenderDocデバッグ により、問題追跡が容易
- GPU・OS間の一貫性向上 により、OpenGLより予測可能な動作を実現
- 新しいシェーディング言語(slang, shady) など拡張の可能性を確保
- より少ない抽象化と明確なパイプライン制御 により保守性が向上
今後の計画
- SDFフォント, 並列イメージロードとミップマップ生成, Bloom, ボリューメトリックフォグ, アニメーションブレンディング, レンダーグラフ, AO を追加予定
- Vulkanの学習は難しいが、現代的なグラフィックスAPIの理解とエンジン設計力の強化 に大いに役立った
1件のコメント
Hacker Newsのコメント
1年前に投稿した自分の投稿以降も、Vulkanに対する考えは大きく変わっていない
低レベルなグラフィックス制御を求める人には面白いだろうが、自分にとっては本当に扱いづらいAPIだった
ゲームエンジンを自作したい気持ちは今もあるが、Vulkanの初期設定ですらいまだに怖い
自分が欲しいのは、SDLが2Dグラフィックスを扱うやり方の3D版のようなもの
SDLで3Dをやろうとすると結局OpenGLまで降りる必要があるが、それは自分の望むレベルではない
もしかするとWebGPUが自分にとって楽しく扱える代替になるのかもしれない
自分もそれでエンジンを1つ作ったが、結局もっと多くの制御と性能が欲しくなってVulkanベースのエンジンに戻った
それでもSDL GPUのコードから同期パターンを学べて、Vulkanエンジンでは大いに役立った
wgpuは、WebGPUレベルの抽象化を提供する中間地点だOpenGLより強力だが、リソースバリアやレイアウト遷移のような詳細を自分で扱う必要がない
ランタイムが一部のbookkeepingを肩代わりしてくれる一方、単一キューしかサポートしないなどの制約がある
Vulkanは大変だが、主要ベンダーがサポートする拡張機能を使えばかなり改善される
ただし、ドライバが無視する細かな設定を要求するなど、依然として不要な複雑さがある
今は高レベルのゲームエンジンと低レベルのVulkan/Metalの間にある中間APIが消えてしまっている
初心者が3Dグラフィックスを学ぶには、シェーダーやバッファのような概念を知らなくてもよい、単純な「三角形を描く」レベルのAPIが必要だ
Vulkanの細かな制御はごく少数のエンジン開発者にしか必要なく、大半にはOpenGLレベルで十分だ
3Dは2Dより組み合わせ可能な要素がはるかに多く、単純なグラフィックスAPIでは扱いきれない
OpenGLも元はそれを目指していたが、結局複雑になった
「bike shedding」は、些細な問題にこだわって重要な部分を見失う行為を意味する
原文で描写されているのは、むしろfeature creepやover-engineeringに近い
プロジェクトの進展を妨げ、個人的な楽しみにばかり集中する行為を指す
bike sheddingはよく「家が完成する前に自転車小屋の色から決めること」で説明される
「Minecraftマルチプレイヤークローンでエンジン開発を始めるのはよくない」という話もあったが、
実際には多くの人が最初のエンジンプロジェクトとしてMinecraft系のゲームを作っている
ボクセルエンジンでは、それが一種の「Hello, world」だ
(2024) 当時の投稿は625ポイント、260コメントを記録した — 元記事リンク
Vulkanは自分が学んだ中で最も難しい技術だった
あまりに直感的でなく、反復作業も多くて、プログラミングの楽しさを奪ってしまう
もっと簡単に始めたいなら、OpenGL、特にシェーダー導入前のバージョンを勧める
ただし業界はOpenGLを徐々に押し出しつつある
チュートリアルに従ってコードを写していただけで、概念を理解できていなかった
そこでWebGPU(Google Dawn) に切り替えたところ、Vulkanよりずっと単純だった
WebGPUの制約のおかげで概念を身につけ、その後で再びVulkanを学ぶとずっと楽になった
WebGPUにはpush constantがなく、pipeline爆発の問題もあるが、Vulkanは同期とメモリ管理のほうが難しい
SDL_GPUも同程度のレベルのAPIなので、入門用としてよい
Vulkanは過剰設計されたAPIだ
CUDAなら1行でできるGPUメモリ割り当てが、Vulkanでは大量のボイラープレートを要求する
最近のVulkanはかなり改善されたが、それでもまだ先は長い
SDL3やwgpuがこの複雑さを和らげる抽象化レイヤーになってくれることを望む
ValveがSDL3を支援しているので、そちらが有力だと思う
まず「グラフィックスをマルチスレッド処理する必要があるのか?」と自問すべきだ
そうでないなら、Vulkan/DX12を使う理由はない
性能問題が出るまでは、OpenGL、DX11、あるいはゲームエンジンを使うほうがずっとよい
自分は3D/ゲームプログラミングに魅了されていて、何人かのYouTuberがゲームを作る過程をよく見ている
だが、WebアプリやDevOpsと比べるとはるかに複雑な世界だ
ピクセルシェーダー、コンピュートシェーダー、ジオメトリ、線形代数、PDEまで登場する
TokyoSpliff YouTubeチャンネル
最近は趣味でゲームエンジンを作ることがかっこいいことと見なされているのがうれしい
自分も10年間個人エンジンを開発しているが、とてもやりがいのある経験だった
グラフィックスプログラミングを初めてやるならOpenGLから始めるのがよい
23年前にNeHeのOpenGLチュートリアルを読んだが、今でも最もよく整理された学習資料の1つだと思う
念のため言っておくと、自分は元記事の執筆者ではなく、タイトルだけをそのまま残した