はじめに
この記事はグラフィックス全般Advent Calendar 2024の23日目の記事です。
皆さんこんにちは。@emadurandalと申します。自作3Dライブラリをせこせこ作っている人です。
これまではTypeScriptで開発していた(RhodoniteTS)のですが、C++でネイティブ系3DAPIの経験も積みたいと考え、C++製ライブラリの開発を始めました。
正直、まだお披露目できるような段階では本来全くないのですが、あえて自分を追い込む意味で、現状で内情を書いてしまおうという無茶な記事です。
名前は Kairos(カイロス)
ライブラリの名前はKairos(カイロス)です。ギリシャ語で時間という意味です。実はギリシャ語で時間という言葉は二つありまして、いわゆる過去から未来へ流れていく時間はKhronos(クロノス)といい、印象的なシーン一つ一つの時を指す言葉がKairos(カイロス)です。
設計面での特徴
Kairosにおける主な特徴を紹介します。
ECS(Entity Component System)
以前から開発していたTypeScript製のRhodoniteTSはコンポーネント指向の設計を導入しましたが、メモリ設計以外、実態はUnityのGame Objectに近く、本当はECSにしたかったのです。
Kairosでは念願のマルチスレッド対応のECSを導入しています。
TEST(ECSMultiThread, ECS_AddVelocity) {
ECS::Repository::reset();
ECS::Repository ecs(10);
float value = 0.f;
// エンティティの作成とコンポーネントの追加
for (int i = 0; i < 1000000; ++i) {
ECS::Entity e = ecs.createEntity();
ecs.addComponent(e, Position{static_cast<float>(i), static_cast<float>(i)});
ecs.addComponent(e, Velocity{static_cast<float>(i), static_cast<float>(i)});
}
util::ProfilingFunction("ECS_SingleThread", [&]() {
ecs.update<Position, Velocity>([&](ECS::Entity entity, Position &position, Velocity &velocity) {
for (int i = 0; i < 1; ++i) {
position.x += velocity.dx;
position.y += velocity.dy;
}
value += position.x;
value += position.y;
});
});
util::ProfilingFunction("ECS_MultiThreads", [&]() {
ecs.updateInMultiThreads<Position, Velocity>([&](ECS::Entity entity, Position &position, Velocity &velocity) {
for (int i = 0; i < 1; ++i) {
position.x += velocity.dx;
position.y += velocity.dy;
}
value += position.x;
value += position.y;
});
});
std::cout << value << std::endl;
}
メモリクラス(Buffer, BufferView, Accessor)
自由なレイアウトで確保したメモリにアクセスできるよう、Buffer, BufferView, Accessorというメモリ管理クラスを作っています。これはRhodoniteTSから継続している設計方針です。気づかれた方もいらっしゃるかも知れませんが、これはglTF2.0のメモリモデルを参考にしています。
TEST(KAccessor, takeOneAsFloat4) {
array<float, 10> data = { 0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f };
std::size_t byteSize = data.size() * sizeof(float);
shared_ptr<Memory::Buffer> b = Memory::Buffer::alloc(byteSize, data.data());
auto oBufferView = b->takeBufferView(byteSize, 4*4);
shared_ptr<Memory::BufferView> pBufferView = *oBufferView;
auto oAccessor = pBufferView->takeAccessorVector4<float>(2);
if (!oAccessor) {
assert(false);
}
shared_ptr<Memory::AccessorVector4<float>> pAccessor = *oAccessor;
EXPECT_EQ(pAccessor->getComponentType(), ComponentType::Float);
EXPECT_EQ(pAccessor->getCompositionType(), CompositionType::Vec4);
auto one = pAccessor->takeOne();
if (!one) {
EXPECT_STREQ(one.error().c_str(), "out of range");
}
EXPECT_EQ((*one.value())[3], 3.0f);
auto two = pAccessor->takeOne();
if (!two) {
EXPECT_STREQ(two.error().c_str(), "out of range");
}
EXPECT_EQ((*two.value())[3], 7.0f);
}
Kairosの描画システム
後述するHermesを利用して描画を行います。つまり、Kairosでの描画処理はHermes APIを使ってコーディングすることで、Vulkan, WebGPU, Metal, Direct3D12すべてに対応できます。
3D API抽象化ライブラリ Hermes(へルメス)
ヘルメスともエルメスとも発音できるみたいですが、Kairosの中でも3DAPIを抽象化するレイヤーにつけた名前です。
Vulkan、Direct3D 12, Metal, WebGPUの4つのAPIを抽象化します。いわゆるRender Hardware Interface (RHI)というレイヤーになります。WebGPUバックエンドではネイティブ動作だけでなく、EmscriptenによりWebでの表示にも対応します。これにより、Webを含めたマルチプラットフォーム展開が可能になります。
WebGPUの実行レイヤーとしては、GoogleのDawnライブラリを採用します。
マルチプラットフォーム対応するのなら、WebGPU(Dawn)を使うだけでいいんじゃない?という声がありそうですが、いいんです。だって各種3D APIの練習したいんだもーん!
……沼って開発がエターナル気がしなくもないけどね_(:3 」∠)_
Hermesの設計方針
HermesのAPI体系はWebGPUに極めて近いものにしています。Hermesのバックエンドで一番機能が少ないのがWebGPUですので、それに合わせるのが妥当だと判断しました。
ただ、WebGPUでサポートしているOveridable Constants(VulkanにおけるSpecialization Constants)をDirect3D12(というかDXIL)がサポートしていないので、Overidable Constants的な機能をどうDirect3D12バックエンドで実装するかが悩みどころです。
シェーダーの扱い
色々迷ったのですが、シェーダーはWGSLで書き、nagaで各API向けにコンバートする方向で行くことにしました。
naga(おそらくtintも)での頂点シェーダーの変換は、各APIの座標系の違いを合わせるためのグルーコードも出力してくれるようですね。
使用ライブラリ
SDL2
プラットフォーム抽象化で使用しています。Vulkan, Metal, WebGPU, Direct3D12すべてサポートしているのは良いですね。
そのうちSDL3にアップデートしようかと考え中です。色々と新機能が追加されているようです。
stb
PNG画像の読み書きで使用しています。
WebGPU-distribution
WebGPUをC++で使うために使っています。バックエンドにwgpuまたはDawnを選べます。KairosではDawnを使用しています。
sdl2webgpu
SDL2でWebGPU向けのサーフェイスを取得するためのライブラリです。
Metal C++
Appleが配布している、C++からMetalを使うためのライブラリです。なぜGithubでホスティングせずダウンロード形式にしているか謎ですね。
pixelmatch-cpp17
レンダリングした画像を使った画像比較によるビジュアルテストのために使用しています。
使用ツールチェイン/IDE
CMake
定番ですね。最近はBezelなどより新しいビルドシステムもありますが、世の中的な普及度はまだCMakeの方が高いようなので、長いものに巻かれています。CMakeといっても最近ではモダンな書き方というのがあり、モダンスタイルで行くのが推奨されているようです。
Google Test
ユニットテスト・ビジュアルテストに利用しています。CMakeの設定でGoogle Testが自動的に導入されるようにしています。
CLion
CLionはJetBrains社が提供する優れたC++ IDEです。Clang-tidyなどがデフォルトで有効になっているので、「ここはこういう指定を追加した方がいいよ」みたいな提案を常にしてくれるのでコードが堅牢になります。
CMakeのサポートも非常に高く、自動的に検知してConfigureしてくれます。
Visual Studio 2022
Windowsでの開発にはVisual Studioを使っています。CLionでもできるはずなのですが、なぜかうまくいかなかったので。
使用を断念したもの
vcpkg
vcpkgはMicrosoftが開発するC++向けパッケージマネージャーです。
非常に便利なのですが、残念ながらvcpkgはネスト使用に対応していません。つまり、vcpkgを利用しているライブラリをvcpkgで導入することができないのです。
そのため、Kairosではvcpkgを使わずにgit submodule + CMakeで依存ライブラリを導入しています。
開発進捗
まだ始まったばかりで、現在のところ頂点をレンダリングすることしかできません。
(ここまで長々と語っておいてこういうオチ……)
これからですね。
最後に
まだこんな進捗状態で公開するのは正直気が引けたのですが、自分を追い込む意味で書きました。
来年どれだけ進むかな……。
明日の記事は @emadurandal さんの「TypeScriptの最新機能でWebGL/WebGPUリソースを自動解放する」です。
って俺じゃん。