Vulkanを学んで小型ゲームエンジンを作るまでの過程
(edw.is)- OpenGLの経験があった開発者が、Vulkanを初めて学んで約3か月で2つの小さなゲームデモと再利用可能なEDBRエンジンを作成した
- 最初から汎用エンジンを設計せず、まず小さなゲームを作ってから必要な部分だけをエンジン化し、過剰な設計やbike-sheddingを減らした
- エンジンは19k LoC規模で、グラフィックスコード6.7k LoC、Vulkan軽量抽象化2k LoCで構成され、コンピュートスキニング、CSM、PBRシェーディング、MSAA、ポストFX、UIレンダリングを含む
vk-bootstrap、Vulkan Memory Allocator、volk、VK_KHR_dynamic_rendering、プッシュ定数、バッファデバイスアドレス、bindless descriptorを使い、Vulkanのボイラープレートとdescriptor setの使用を減らした- Vulkanはグローバル状態の排除、より優れた検証エラー、RenderDocでのシェーダーデバッグ、GPU・OS間の一貫性を提供したが、明示的同期は依然として自分で扱う必要がある
3か月で作ったVulkanベースのEDBRエンジン
- EDBR(Elias Daler’s Bikeshed Engine)はVulkan学習プロジェクトとして始まり、その後のプロジェクトに再利用できる小さなエンジンへと発展した
- エンジンとゲームコードはGitHubリポジトリで公開されている
- 執筆時点でのコード規模は次の通り
- エンジン本体: 19k LoC
- グラフィックス関連コード: 6.7k LoC
- Vulkan軽量抽象化: 2k LoC
- 3D猫ゲーム: 4.6k LoC
- 2Dプラットフォームゲーム: 1.2k LoC
- エンジン本体: 19k LoC
- 入力処理やオーディオシステムなど一部の非グラフィックスコードは以前のエンジンから持ってきたが、グラフィックスと複数の中核システムは新たに書いた
- Vulkanを既存のOpenGL抽象化に押し込むより、新しく書き直すほうが適していたと評価している
グラフィックスプログラミングからVulkanまでの学習順序
- グラフィックスプログラミングを初めて始めるなら、VulkanよりOpenGLから始めるほうが複雑さに圧倒されにくい
- 最低限の目標としては、テクスチャを適用したモデルを画面に表示し、簡単なBlinn-Phongライティングを実装するレベルを勧めている
- 基本的なshadow mappingは、シーンを別視点・別レンダーターゲットでレンダリングし、depth textureをサンプリングする方法を学ぶのに役立つ
- OpenGLの学習資料として次を勧めている
- Vulkan学習にはvkguideが最も役立ち、最初に始めるなら全体を追ってよいが、簡単なゲームでは「GPU driven rendering」レベルの複雑さは今すぐ必要ないかもしれない
- Vulkan Lecture Series by TU WienはVulkanの基礎を扱っており、特に同期の講義が役立つ
- 最初の1か月の学習成果として次の機能を実装した
- glTFモデルローディング
- コンピュートスキニング
- frustum culling
- shadow mappingとcascaded shadow maps
Vulkanを選んだ理由とWebGPUとの比較
- 目標はWindowsとLinux中心のデスクトップ向け小規模3Dゲームで、オープンソース技術とオープン標準を好んだため、OpenGLとVulkanの間で選択した
- OpenGLは小さなゲームには十分だが、新バージョンが出る可能性は低く、macOSではdeprecated状態のため将来が不透明だと見ている
- WebGPUも一部学習したが、次の制約があった
- まだ安定しておらず、チュートリアルやサンプルが多くない
- WGSLの文法はGLSLほど好みに合わなかった
- デスクトップではDirectX、Vulkan、Metal上のラッパーに近く、プラットフォームごとにRenderDocキャプチャが異なり、WebGPU呼び出しとネイティブAPI呼び出しが1:1で対応しない
- bindless texturesとpush constantsがない
- WebGPUにも明確な利点がある
- OpenGL/WebGLより検証エラーが優れており、グローバル状態がない
- Vulkanと似た部分があり、Vulkan学習前の助けになる
- Vulkanより画面に何かを表示するために必要なボイラープレートが少ない
- 明示的同期を直接扱う必要がない
- ブラウザでゲームを実行できる
フレームレンダリングの流れ
- 1フレームはいくつかの段階に分かれ、各段階はパイプラインまたはパスの形で実装される
- スキニング段階では、skeletal animationのあるモデルをcompute shaderで処理する
- 入力はスキニングされていないmeshとjoint matrices
- 出力は後続のレンダリング段階で使うvertex buffer
- 以後の段階では、静的meshとskinned meshをほぼ同じように扱える
- CSM段階では、4096x4096のdepth textureと3つのsliceを使ってcascaded shadow mappingを行う
- geometry + shading段階では、モデルを描画し、shadow mapとlight情報を使ってシェーディングする
- PBRモデルはPhysically Based Rendering in Filamentで説明されている方式とほぼ同じ
- fragment shaderは1回のdraw callで、そのmeshに影響するすべてのlight計算を行う
- すべてはmulti-sampled textureに描画された後、resolveされる
- depth resolveはfragment shaderで手動処理する
- multi-sample depth textureのすべてのfragmentを走査し、最小値をnon-MS depth textureに書き込む
- post FX段階では現在depth fogのみを適用しており、今後はtone mappingとbloomもこの段階で処理する予定
- UI段階ではdialogue UIを描画し、1回のdraw callで処理する
Vulkanボイラープレートを減らすライブラリ
vk-bootstrapはphysical device選択、swapchain生成などVulkan初期化のボイラープレートを減らす- Vulkan関数全体のラッパーではなく、主に初期化段階に影響する
- Vulkan Memory AllocatorはVulkanメモリ割り当てを直接扱わずに済むようにしてくれる
volkはextension function loadingを単純化する- たとえば
vkSetDebugUtilsObjectNameEXTのようなextension関数を、直接ポインタとして保持せずに使える
- たとえば
GfxDeviceクラスは、よく使うVulkan機能とオブジェクトをまとめている- Vulkan contextの初期化
- swapchainの生成と管理
beginFrameとendFrame- image生成とtextureロード
- buffer生成
- bindless descriptor setの管理
GfxDevice.cppは執筆時点で714行で、VkDevice、VkQueue、VmaAllocatorなどを各所へ渡す代わりに、1つのオブジェクトを渡す方式が便利だとしている
シェーダービルドとdescriptor set回避戦略
- シェーダー言語はOpenGLの経験があったため GLSL を選択
- シェーダーはランタイムではなくビルド段階でSPIR-Vにコンパイル
- ランタイムのshaderローディングコードが単純になる
- ランタイムshader compilerへの依存がない
- shaderエラーをビルド段階で発見できる
glslcはCMakeのDEPFILEを指定できるため、shader includeの変更時に関連ファイルを自動で再コンパイルできる- Vulkanではuniformをdescriptor setとしてまとめる必要があり、OpenGLよりデータ受け渡しが複雑になる
- 実装ではdescriptor setの使用を大幅に減らした
- bindless textureとsampler用のグローバルdescriptor setを1つだけ使用
- それ以外の大半は push constants で渡す
- buffer device addressを使ってbufferアドレスをpush constantsで渡す
pipeline classとdynamic rendering
- レンダリング段階は
PostFXPipelineのような pipeline class として分離 - 各pipelineは通常、次の役割を持つ
init: shader読み込み、VkPipeline、VkPipelineLayoutの初期化cleanup: pipelineとlayoutの解放draw: 毎フレーム必要な入力を受け取ってdraw callを実行
drawはvkCmdBeginRenderingとvkCmdEndRenderingの間で呼ばれることを前提としている- render passがどのtextureにレンダリングするかはpipeline内部では気にせず、呼び出し側がrender targetを決める
VK_KHR_dynamic_renderingを全体的に使い、Vulkanのrender passとsubpassは使わない- tile-based GPUではrender passとsubpassのほうが効率的だと聞いているが、現時点ではモバイル対応は考慮していない
- dynamic renderingによって実装が大幅に簡単になる
PVP、BDA、bindless descriptorの活用
- すべてのmeshで1つのvertex型を使用
- programmable vertex pulling を使うと、OpenGLのVAOやVulkanの
VkVertexInputBindingDescription、VkVertexInputAttributeDescriptionのようなvertex format定義を避けられる - buffer device address を使うと、vertex bufferをdescriptor setにバインドせずにbufferアドレスをpush constantsで渡せる
- push constantsとbufferには
scalarレイアウトを使用std430よりアラインメント処理が容易で、C++構造体とほぼ同じ感覚で扱える- C++構造体でpaddingメンバーが必要になる場面が減る
- bindless descriptorは、大きなdescriptor set内にtextureとsamplerの配列を置く形で使う
- 新しいtextureをロードしたら
textures配列に入れ、そのindexをbindless texture IDとして使う - shaderにはtexture IDをpush constantsで渡す
- 新しいtextureをロードしたら
- samplerはimageと分離し、共通samplerをstartup時に作成して
samplers配列に入れる - material bufferでもbindless texture IDを使う
- material IDだけをpush constantsで渡し、fragment shaderでmaterial bufferを参照する
- 大がかりなdescriptor setなしで、materialごとに整数1つでtextureへアクセスできる
- bindless texture関連の参考資料として Vulkan Bindless Texture を勧めている
毎フレームアップロードされる動的データ
- 毎フレームCPUからGPUへ送る必要があるデータは、大きな配列をあらかじめ確保しておき、毎フレームindex 0から埋めていく方式を使う
- 例として、すべてのjoint matrixを1つの大きな
mat4配列に保存し、skinned meshごとに開始indexをpush constantsで渡す - 方法は2つある
- GPUにN個のbufferを置き、frame-in-flightごとに切り替える
- GPUには1つのbufferだけを置き、CPU側のstaging bufferをN個用意する
- ほとんどの場合、1つ目の方式が推奨される
- GPUメモリはより多く使うが、手動同期が不要
- GPUメモリを節約したいなら、2つ目の方式が有用な場合がある
- 両者の間で目立った性能差は見つからなかったが、毎フレーム非常に大きなデータをアップロードするなら差が出る可能性はある
cleanupと同期
- vkguideのdeletion queueパターンは、筆者のエンジンではあまり有効ではなかった
- 毎フレーム新しいVulkanオブジェクトを割り当てたり破棄したりしないため
- C++ destructorベースのcleanupも使いやすくはなかった
- wrapper class、move constructor、move assignmentが必要になり、複雑さが増す
- フレーム途中で使用中のオブジェクトをwrapper破棄によって誤って消してしまう危険がある
- 現在は
cleanup関数を明示的に呼び出して、Vulkanオブジェクトを1か所で整理している- 呼び出し忘れは起こりやすいが、終了時にVulkan validation errorとVMA assertがcleanup漏れを教えてくれる
- Vulkanの synchronization は難しく、明示的に管理する必要がある
- OpenGLやWebGPUはtextureやbufferの読み取りに必要な同期を代わりに処理してくれる
- Vulkanではdata raceを避けるためにbarrierを自分で入れなければならない
- 現在はdrawをpassとpipelineに分け、その間にbarrierを手動で挿入している
- たとえば、compute shaderのskinning passがvertex dataを書き込んだあと、shadow mapping passがそれを読み取る前にbarrierを入れる
- render graphで自動化することもできるが、現状は手動同期で満足している
vkconfigのsynchronization validation layerは同期エラーを見つけるのに役立つ
sprite、skinning、game/renderer分離
- bindless textureを使うと、多数のspriteをvertex bufferなしで1回のdraw callで描画しやすい
- sprite vertex shaderは
gl_VertexIndexでquadのvertex coordinateとUVを生成する - すべてのsprite draw callは
SpriteDrawBufferにSpriteDrawCommandとして集約される- transform
- UV範囲
- color
- texture ID
- shader ID
- 実際のdraw callは
vkCmdDraw(cmd, 6, spriteDrawCommands.size(), 0, 0)の形になる- spriteごとに6 vertex
- sprite数ぶんinstance
- sprite rendererは1万個のspriteを 315マイクロ秒 で描画できる
- コンピュートスキニングは、skeletal animationを持つmeshで入力vertexとjoint matricesを受け取り、skinned vertex bufferを生成する
- 同じmeshを持つ3匹の猫でも、それぞれ異なるanimationを持てる
- 出力vertex bufferはmesh instanceごとに必要
- game logicとrendererはdraw commandで分離されている
- game logicは entt を使用
- rendererはentityやgame objectを知らず、light、scene parameter、mesh draw commandだけを扱う
MeshDrawCommandにはmeshId、transform matrix、bounding sphere、skinned meshポインタ、joint matrix開始index、shadow castingの有無が含まれる
シーン読み込み、UI、Dear ImGui
- レベルエディタは自作せず、Blenderを使って glTF にエクスポートした
- レベルエディタを自分で実装すると数か月から数年かかる可能性があるため、時間を節約した
- ノード名で prefab の生成と physics shape の指定を行う
- 例:
Interact.Sphere.Diaryでは、最初のドットの前にあるInteractが prefab 名 Sphereは physics system で sphere physics body を作るときに使うCapsule、Boxも使え、なければ mesh vertex から physics shape を生成する
- 例:
- 複雑な model は level glTF に直接入れず、
Empty->Arrowsobject として配置してからCat.NearStoreのような名前を付けるCatprefab を生成し、NearStoretag を付ける
- prefab は JSON で記述し、外部 glTF や movement、physics 情報を含められる
- UI システムは Roblox UI API から着想を得た
- origin
- relative size
- relative position
offsetPosition,offsetSize- fixed size
- label/image content ベースの size
- UI element の size を再帰的に計算した後に position を計算し、parent から children の順に描画する
- Dear ImGui は開発・デバッグツールに使っている
- sRGB framebuffer で Dear ImGui が正しく表示されない問題があったため、独自の Dear ImGui backend を作成した
- rendering 部分だけを実装し、input event 処理や clipboard などの logic/OS interaction は標準の Dear ImGui SDL backend が担当する
- 独自 backend の利点は次のとおり
- bindless texture id をサポートしており、
ImGui::Image(bindlessTextureId, ...)で image を描画できる - linear image と non-linear image を format の受け渡しによって正しく描画できる
- エンジン内の他の Vulkan コードと同じ方法で初期化して扱える
- bindless texture id をサポートしており、
使用したライブラリと Vulkan 移行の効果
- 物理には Jolt Physics を使用した
- 主に collision resolution と basic character movement に使っている
JPH::CharacterVirtualが基本的な character movement をうまく処理してくれる
- ECS には entt を使用した
- 自作 ECS ではなく外部ライブラリを使うことで、保守するコードを減らした
- オーディオには openal-soft、libogg、libvorbis を使用した
- profiling には Tracy を使用した
- どのコードが実際にどれだけ短い時間しか使っていないかを確認でき、不要な bike-shedding を避けるのに役立った
- Vulkan へ移行して得られた点は次のとおり
- OpenGL のグローバル状態がなくなり、抽象化しやすくなった
- OpenGL 流の
shader.bind()や state tracker、magic RAII があまり必要なくなった - validation error が OpenGL よりも充実している
- RenderDoc で vertex shader と fragment shader を直接デバッグできる
- GPU と OS 間の動作差が OpenGL より目立ちにくい
- 今後 Slang や Shady のような別の shading language を探れる
- グラフィックスパイプラインの各側面をより細かく制御できる
今後の作業
- 今後予定している作業は次のとおり
- signed-distance field font のサポート
- 大量の image 読み込みと mipmap の並列生成
- bloom
- volumetric fog
- animation blending
- render graph
- ambient occlusion
- ゲームの完成
- Vulkan の学習は大変だったが、予想していたほど難しくはなく、グラフィックスプログラミングと modern API をより深く理解するきっかけになった
1件のコメント
Hacker News のコメント
ミニマリズムは非常に効果的
私は逆の道を行き、そのせいで大きな苦労をしている。Rustでメタバースクライアントを作っていて、今も別の画面では、アバターが巨大なスチームパンク都市をトラムで通り抜けるシーンを表示している。新しいプレリリースを出す前に、12時間ずつ動かして確認している
Vulkanの上にWGPUとRend3を載せており、Rend3はメッシュ、2Dテクスチャ、オブジェクトを作ると、オブジェクトがメッシュとテクスチャを参照して画面に現れる、すっきりしたAPIを提供している。Rustの参照カウントがつながりを保ってくれるので、書くのは直感的だ
しかしレイヤーが増えると問題が出てくる。WGPUはWebブラウザ、Vulkan、Metal、DX11(最近削除された)、DX12、Android、OpenGLまでサポートしようとするため、大きな開発チームが必要で、変更も難しい。WGPU API自体も概ねVulkanに似ているので、GPUメモリの割り当てと同期を自分で気にする必要がある
WGPUには最小公分母の問題がある。あるプラットフォームでは特定の機能をサポートできず、Vulkanがサポートする複数スレッドからのGPUメモリ同時更新を、WGPUは干渉なしには処理できない。大きな世界を扱うゲームやクライアントには、フレームレートを殺さずにコンテンツをGPUへ投入するこの機能が必要だ。プラットフォームごとに並行性の制約が異なるため、ロック競合で性能が大きく落ちることもある
Rend3は同期と割り当てを処理する適度なグルーコードの役割を果たそうとしたが、特に同期を一般的に解くのは難しい。視錐台カリングは大きな性能向上になるが、オクルージョンカリングは計算コストのため逆効果だった。半透明処理も深度ソートが必要で厄介だ。窓の多い世界を扱うので、外を見たり中を覗いたりできる半透明オブジェクトが不可欠になる
Rust 3Dスタックの人たちは、私が3年間にわたってスタックを直すよう圧力をかけ続けたので、うんざりしているようだ。全員がボランティアで、Vulkanは資金とユーザーベースがあるから維持されている。Rend3は最近作者が諦めたので、今度は私が中に入って直さなければならない。WGPUで複雑なものを作る人はまれで、ほとんどはFlashでも可能だったような2Dゲームや、単純な静的3Dシーンだ。商用プロジェクトは相変わらずUnityやUE5を使っている
Vulkanへ直接行くとしても、同期、割り当て、視錐台カリング、半透明処理を自分で書かなければならないので、大きな転換になる
付け加えると、VulkanとMetalの上のラッパーであるVulkanoにも最小公分母の問題がある。VulkanとMetalはいずれもGPUアセットの同時更新をサポートしているが、Vulkanoは許可していない。もちろんAppleはまた別の扱いをする
https://gpuweb.github.io/gpuweb/explainer/#multithreading
https://github.com/gpuweb/gpuweb/issues/354
OpenGLもスレッドをサポートしたことがないので、OpenGLを使うのはそういうことにはならない
1年少し前にVulkanを学ぼうとしたが、二度と触りたくない。OpenGLを廃止して、単純なことまで途方もなく難しくするものに置き換えるのは本当に不便だ。たとえば回転するキューブ1つにも数百行のコードが必要になる
OpenGLも簡単ではなかったが、普通の人が比較的短い時間で基礎を学ぶことはできた。大型書店でグラフィックスプログラミングの入門書を買えば、午後1日か2日で基本的なレンダリングを表示できた。Vulkanがある面ではより優れているのだろうが、短期間で学べると期待するのは現実的ではない
最新のIntel/ARM/AMDチップが出てきて、CやC++は使えず、「高水準言語のサポートをやめるので、これからはアセンブリだけを使え。制御できることが増えるから速くなる」と言われるところを想像してみると、当然あり得ない話だと言われるはずだ
Vulkanは高水準グラフィックスAPIというより、「GPU API」に近いとよく言われる。そう考えると複雑さは驚くことではなく、そもそもドメイン自体が難しい
最新CPUで高水準言語をなくしてアセンブリだけを使えという比喩よりは、シングルスレッドのC/C++とマルチスレッドのC/C++の違いに近い。複雑度は大きく増し、知らないと壊れたり性能が悪化したりするが、前に進むための実用的な道でもある
OpenGLは概ねVulkanの上に実装できる。OpenGL標準がもはや活発に開発されていないのは残念だが、永遠のものはない
問題は、OpenGLがもはや現在のハードウェアとうまく合っておらず、素朴な使い方をすると性能が非常に非効率になる点にある。OpenGLでもドライバ負荷をなくすような手法まで行くと、Vulkanがそこまで難しいわけではない
現在のIntel/ARM/AMDチップも、CやC++を直接サポートしているわけではない。すでにアセンブリを使うか、C/C++から変換してくれるサードパーティ製ツールを使っている。Vulkanの目的も、GPUに対する低水準の標準インターフェースを提供し、その上により使いやすい抽象化を載せられるようにすることだ
https://github.com/google/angle
多くのスマートフォンは今や、Vulkanドライバ上での唯一のOpenGLサポートとしてANGLEを搭載している
比較的簡単で移植性のある現代的なAPIが欲しいなら、RustのwgpuやC++のdawn経由でWebGPUを使えばよい
Vulkanは素晴らしいと思うが、その目的は高度なGPU機能を最大限活用することにある。高度なGPU機能を使う場合、OpenGLより高い性能を出せることもある
高度なレンダリング手法を目指していないなら、通常はOpenGLが推奨される道だと感じる
今でも2D、ローポリ、PS1風グラフィックのゲームは多く、こうしたゲームにVulkanを使う必要はない
Vulkanは、AAAゲーム業界がレンダリング品質と見た目の方向に傾いてきたことの一例だ。AAAスタジオは非常に高度なエンジンとコンテンツで予算を正当化するが、プレイヤーがそれに疲れ、グラフィックよりゲームプレイを求めていることに気づき、2D/ローポリゲームの市場が大きくなっている
ゲーム開発者なら、レンダリング品質よりゲームプレイと機能に集中したいはずだ
グローバル状態がなく、実行時に使うGPUを選択でき、OpenGLのエラー処理はひどい。検証レイヤーがあり、公式アートワークも格好よく、ドキュメントも優れている。2つ目のスレッドで非同期にGPUへデータをアップロードすることもより合理的に可能で、メッシュシェーダーやRTXのような高度なGPU機能もある
消費者はAAA級グラフィックのインディーゲームを拒むだろうか? そうではないはずだ。そういうゲームが少ない理由は、財政的に成り立ちにくく、より様式化された低忠実度のグラフィックを受け入れる市場が十分に大きいからだ
同時に、WebGLでは使えないコンピュートシェーダーのような現代的機能にアクセスでき、OpenGLのように同じことをする複数の方法が積み重なったレガシーも多くない。主な利点は新しいことだが、そのためチュートリアルがはるかに少なく、これはかなり深刻な欠点だ
フレームワークやエンジンは際限なくあるが、大半は未完成で、どれもコード構造について強い意見を持っている。「コールバックを入れるシーングラフを作れ。いや、エンティティとコンポーネントクラスとオブジェクトを書け。待て、今は全部即時モードで関数型でステートレスだ」といった具合だ
ここにプラットフォームの混乱まで加わる。モバイルゲームのプッシュ通知、アプリ内購入、Xcode経由の必須署名まで合わさると、完全にめちゃくちゃだ。Unityが市場シェアを持っているのには理由があり、それは優れたソフトウェアだからではない。派手なWebView以上のクロスプラットフォームは、今なお途方もない苦痛だからだ
GPU/ドライバ別のコードを書き続けなければならないなら、抽象化レイヤーの意味がない。低水準ドライバと高水準ライブラリを分離するほうがよい
この記事には良いアドバイスが多い。特に「今必要でなければ実装するな」が目を引いた。
これは、数年の経験はあるもののまだ成長途上のジュニアプログラマーたちと、いつもぶつかる部分だ。彼らは「ベストプラクティス」や流行りの派手な新ツールにこだわることが多いが、解くべき問題から出発し、その問題を解決するために必要な最小限に集中するのに苦労している。
中規模から大規模の組織では、普通はそうはいかない。たいてい次の仕事に移ってしまい、もう一度見直す時間はほとんどない。残念ながら、だからこそ最初からきちんと作り、バグや副作用を生む余地を減らす必要がある。
あまりにも多くのコードベースで、新機能を急ぐあまり地雷原のようなコードになり、小さな変更ひとつにもすべての機能を手動で確認し、アプリケーション全体の文脈を頭に入れておく必要があった。
私の経験では、後から新しい要件が入ってくると、以前に作った汎用的な解決策はまったく不適切で、結局作り直すことになる。目の前の問題を解くべきであって、未知の未来の問題を解こうとしてはいけない。
だから、人々があらゆる機会を見つけてあちこちで何かを自分で書こうとするのも不思議ではない。こうしたデジタル配管工のような雑務は人を疲弊させるので、正気をもう少し長く保つために、時々はもっと楽しい作業を挟みたくなる。
サイトがアクセス集中で落ちたようなので、キャッシュを残しておく: https://web.archive.org/web/20240606103630/https://edw.is/le...
素晴らしい記事だ。科学データ可視化エンジンを作ろうとして Vulkan を独学した: https://datoviz.org/ まだかなり実験的で、近いうちに新バージョンを出す予定だ。
以前から OpenGL の知識はあったが、Vulkan の学習は本当に難しかった。5年前は学習資料もそれほど良くなかった。それでも挑戦し、とても面白かった。
数十種類の抽象化の役割を理解するのに数か月かかり、その過程で Vulkan を少し苦痛なく使うための小さなラッパーも書いた: https://datoviz.org/api/vklite/
このラッパーは、科学可視化で最も必要になる機能の一部だけをサポートしている。
Vulkan で OpenGL より良い性能を出すのは簡単ではない。Vulkan ドライバーには、OpenGL ドライバーがレンダリングパイプラインとレンダーターゲット設定を代わりに処理してくれていた約2万行分のコードが含まれていない。
そのコードはすでに OpenGL ドライバーの中にあり、業界最高の人たちが20年以上にわたって最適化してきたものだ。
だから、OpenGL が標準で提供していた機能と同等のものを Vulkan の上に素朴に組み立てても、魔法のように良い性能が出るわけではない。より多くの作業が必要で、正しいフェンスや同期プリミティブを入れるなど、本当の問題が積み上がり始める。
実際に何をしているのかを理解し、良好な並列性と正しい同期でレンダリングを実行できるようになって初めて、Vulkan の性能上の利点を期待できる。
趣味開発者の私は、シンプルさから OpenGL ES3 を使っている。私にはすでに十分で、面倒な Vulkan の頂点ディスクリプター、ディスクリプター、ディスクリプターを書くよりも急ぐべきことがたくさんある。
参考までに、私のエンジンはここにある: https://github.com/ensisoft/detonator
Vulkan は今ではバインドレステクスチャを許可していると聞いたので、ディスクリプターまわりの混乱は以前ほどひどくはないのかもしれない。
Vulkan は魅力的だが、私が払いたくない高い初期コストがある。
その後、そのライブラリをユースケースに合わせて最適化したルーチンへ段階的に置き換えていけばよい。
Vulkan の資料が増えるのは良いことだが、これも画面に何かを表示するすべての Vulkan 資料で見てきたのと同じ問題を抱えている。
どれも単純なケースを示す前に、Vulkan の上にさらに別の抽象化レイヤーを導入している。いつも vk-bootstrap、volk、vma や他のライブラリを使えと言われる。
メモリ管理を手動で行う例を示している資料が一つでもあるのか分からない。vma を使うか、仕様書を直接掘るかの選択肢しかないように見える。Vulkan SDK そのもの以外には何のライブラリも追加しない、最も基本的な例を求めるのはそんなに過剰なことなのだろうか?
ほとんどのゲームには、おおむね 3 種類の寿命がある:永続/起動時、レベル単位、フレーム単位。
これらの寿命は入れ子になるので、1 つのスタックアロケータだけでかなり遠くまで行ける。フレームやレベルが終わったら開始位置に戻せばよい。
もっと複雑なパターンもあるが、この方式だけでもかなり有用で、CPU と GPU の両方で使える。
その意味で、上にライブラリを広範に使うのはごく普通のことだ。今どきのソフトウェアをシステムコールに直接向けて書かないのと同じだ。
ある人の Vulkan 初期化手順を Khronos Group リポジトリのコードと照らし合わせ、Vulkan 1.3 仕様を読んでいるうちに、何かをするには仕様を順番どおりではなく、あちこち混ぜて読まなければならないと気づく瞬間、失敗しているのは明らかになる。
彼らは失敗した。他の基準で見ても出来の悪い仕事だ。ただ、一度やってしまえば大部分は忘れられるので、専門家たちが大きく不満を言わないだけだ。
このスレッドの別のコメントに、仕様の章や節をコメントとして付けたソースコードの一部を残しておいた。SDL などと一緒に使える一般的な実装だ。
執筆時点での標準的なアプローチは、公式 Vulkan SDK に含まれる VMA と Volk を使うことだ。これだけでも現状を十分に物語っている。
数年にわたって断続的に Vulkan を学ぼうとしてきた。以前は OpenGL ES 2 と 3 をかなりよく知っていた。
難しかったことの一つは、サンプルではなく実際のエンジンでどう使うべきかを理解することだった。多くのサンプルは必要な分だけちょうど確保するか、不足しないように数百個確保している。
DirectX を学んだときは Microsoft の MiniEngine が役に立った。複雑すぎず、ディスクリプタ割り当てを管理する DescriptorAllocator のようなものがあったからだ。Vulkan にも似たものがあるのか気になる。
もう一つ難しいのは、マテリアル、メッシュ、レンダリング順序のような良い抽象化をどう作るかを知ることだ。チュートリアルの先へ進むために学ぶのに適したエンジンやフレームワークはあるだろうか?
ディスクリプタセットの割り当てでは、自分にとって筋が通るパターンは一つだけだ。プールは寿命が短く、大量に作られるものだと考えるべきだ。現在のプールで割り当てが失敗したら新しいプールを作り、自前でディスクリプタ数のカウンタを維持しない方式だ。標準は単純なカウントとは異なる、ありとあらゆるプールの挙動を許している。そのプールを参照する最後のコマンドバッファが終わった後で、古いプールを捨てればよい。
パイプラインバリアとイメージレイアウトは本当に厄介だ。すべてのリソースの最後の使用と最後の形式を追跡し、必要なバリアを追加するレイヤーとして抽象化するのがよい。複雑になり得るが、任意のパスがあったり、パスの順序を変えられたりするなど、より複雑な状況が出てくると、それだけの価値がある。
メッシュ、マテリアル、レンダリング順序は HN のコメント一つで要約するのは難しく、レンダリングアルゴリズムの選択に大きく左右される。非常に一般化された解法をきちんと作るためにかかる膨大な労力は、それに見合わないと思う。
Vulkan 1.3 で「Hello, Triangle!」を書くには何が必要なのか気になるライトな読者なら、これを見るとよい:https://github.com/Planimeter/game-engine-3d/blob/main/src/g...
Vulkan の初期化と基本的なスワップチェーン管理は非常に冗長だが、一度やってしまい、後でパイプラインの作成と管理のための便利な抽象化を作れば、ずっと良くなる。