- Wheel Reinvention Jamの「既存ソフトウェアを新しい視点で見直す」という課題から ShapeUp が生まれ、ブラウザで動くデモと
.obj エクスポートまで備えた3Dモデラーとして完成した
- 1週間で3Dツールを作れた鍵は ray marched SDF にあり、三角形ベースのレンダラーよりも、色・ソフトシャドウ・ambient occlusion を備えたシーンを素早く実装できた
- 実装は 単一のCファイル を中心にシンプルに保たれ、モデルは最大100個の Shape を静的配列に格納してメモリ管理の負担を減らした
- raylib は OpenGL ウィンドウを素早く立ち上げるのには役立ったが、
int 中心の API、引数検証の欠如、GLFW 依存、raygui の限界のため、直接 OpenGL を使ったり機能を作り直したりする必要があった
- 最終成果物は C 2024行 と GLSL 250行、合計約2300行規模で、ファイルの読み込み・保存、複数プラットフォームでの実行、
.obj エクスポートに対応した
ShapeUpが3Dモデラーへ変わるまで
- Wheel Reinvention Jam は、既存のソフトウェアシステムを新しい視点から見直す 1週間のプログラミングイベント だった
- 当初の目標は、遅い TypeScript コンパイラへの不満から出発し、
tsc より速い TypeScript サブセットを作ることだった
esbuild や Bun の TypeScript パーサーを出発点にすれば可能に見えた
- しかし成功デモが「あるターミナルコマンドが別のコマンドより速く終わる」にとどまり視覚的な魅力に欠けたため、最終的に3Dへ方向転換した
- ShapeUp は、マウスで図形を編集する3Dモデラーとして作られた
- 以前から SDF シェーダーを書いた経験はあったが、コードを直接編集してモデリングする方式は自然ではなかった
- 目標は SDF ベースの形状編集 をマウスで行えるようにすることだった
SDFが1週間プロジェクトを可能にした理由
- ShapeUp のレンダリング基盤は ray marched signed distance fields(SDFs) である
- SDF シーンは、色、ソフトシャドウ、ambient occlusion を含めても、三角形ベースのレンダラーより速く実装できた
- Inigo Quilez が SDF でその場で Pixar 風キャラクターを作る例が、技術的な方向性の参考になった
- ShapeUp はコード編集の代わりに図形を直接操作する方法で SDF モデリングを扱った
C実装とデータ構造
- ShapeUp は C で書かれ、OpenGL ウィンドウ生成のために raylib を使用した
- C を選んだ理由は、高速コンパイル、複雑な動作を隠さない文法、慣れ、ネイティブと WebAssembly の両方へコンパイルできる可能性にあった
- モデルは
Shape 構造体の集合で構成される
- 各 Shape は位置、サイズ、角度、角の半径、blob の度合い、色、ミラー軸、subtract の有無を持つ
- Shape の一覧は動的割り当てではなく静的配列で管理された
MAX_SHAPE_COUNT は 100
Shape shapes[MAX_SHAPE_COUNT], shape_count, selected_shape で状態を管理する
- この方式により、割り当て失敗やメモリリークの可能性をなくした
- 100個の Shape 制限 は実際の使用では大きな問題にならなかった
- レンダラー最適化の時間が足りず、100個に達する前にまずフレームレートが落ちた
- 時間があれば、モデルを小さなブリック単位に分割し、そのブリック内で ray marching を行いたかったという
メモリ使用の方法
- ShapeUp が動的メモリ割り当てを使うのは 3か所 だけだった
- 保存: 文書全体を格納できるバッファを確保
.OBJ エクスポート: すべての頂点を格納できるバッファを確保
- GLSL シェーダー生成: シェーダーソース用バッファを確保
- いずれの場合も関数の最後で1回だけ
free する
- 各 Shape を個別に
malloc し、そのポインタを動的配列に保存することもできるが、このプロジェクトにはその構造は不要だった
- C にはメモリ配置を直接制御できる利点があった
- 動的配列やハッシュマップが必要なら
stb_ds.h のようなツールも使えた
UI実装の方式
- UI は immediate mode user interface(IMGUI) 方式で実装された
- IMGUI はデバッグしやすく、CSS・constraints・SwiftUI の代わりに実際のプログラミング言語で要素位置を決められる点が利点だった
- フォーカスされた要素やマウス操作は
Control enum で追跡した
- 位置、サイズ、角度、色、移動、回転、スケール、カメラ回転、blob の度合いといった操作状態を enum 値で表した
focused_control と mouse_action が現在の UI 状態を保持する
raylibとrayguiで詰まった点
- raylib は OpenGL ウィンドウを素早く立ち上げるには有用だったが、時間が経つにつれて開発速度を落とす要因になった
- raylib API で特に不便だったのは 型情報の不足 だった
- enum 型が期待される箇所でも
int を使っており、コンパイラの型チェックを受けられない
- 関数シグネチャだけでは引数の意味が分かりにくい
- たとえば
IsGestureDetected(unsigned int gesture) の gesture は登録済みジェスチャー ID のように見えるが、実際には Gesture enum である
- ドキュメントがヘッダーファイル中心のため、どの
int が実際に enum なのか確認するには実装を見る必要があった
- 基本的な引数検証をしない設計も問題を大きくした
LoadFileData(const char *fileName, int * dataSize) は dataSize が NULL だと segfault する
- ヘッダーには
dataSize が出力引数なのか、null 不可なのかが示されていない
- 検証がないため単純な問題の追跡が難しくなり、場合によっては静かに異常動作することもあった
- 依存関係の扱いでも期待との差があった
- GLFW の問題を raylib が回避したりパッチを送ったりしないことがあった
- 最終利用者にとって重要なのは、ウィンドウ生成の内部実装よりも raylib の機能がきちんと動くかどうかだった
- raygui UI ライブラリはプロジェクトで使うには制約が多かった
- 浮動小数点数を表示できず、独自の float テキストフィールドを作る必要があった
- 重なった要素や切り抜かれた要素のマウスイベントルーティングを処理できない
- UI で一般的な角丸をサポートしていない
- 見栄えよくスタイリングしにくい
- バグも開発フローを妨げた
- raygui ツールのバグにより、過度に装飾されたデフォルトフォントを変更できなかった
DrawCircle(...) のような描画関数は三角形間で頂点を共有しないため、現在の行列にスケールや回転があると浮動小数点誤差でピクセルの隙間が生じた
- 見つけた issue はしばらく報告していたが、ほとんどが “wont fix” で閉じられ、その後は報告をやめた
- 回避策は、直接 OpenGL 関数を使うか、必要な機能を一から実装することだった
- 今後は raylib の代わりに sokol を使う予定だという
6日で終わらせる必要があった4つのこと
- ShapeUp は6日で大きく4つの部分を完成させる必要があった
- ユーザーインターフェース: 3D ギズモ、キーボードショートカット、サイドバー、ゲームコントローラー
- GLSL シェーダー生成器と ray marching レンダラー
- GPU ベースのマウス選択
- エクスポート用の marching cubes
- 難しさは各機能そのものより 優先順位を保つこと にあった
- 厄介だったり時間がかかったりする問題は、設計を変えて避けるか、90%のケースで動くシンプルな解決策で処理した
- 機能によっては1日寝かせている間に解決策が思い浮かぶこともあった
- 作業方針は、常に動く3Dモデラーを保ち、時間が許す限り段階的に改善することだった
- 完成時にだけピラミッドになる方式ではなく、どの段階で止めても完成した小さなピラミッドになるように作る方式にたとえられている
最終結果
- 1週間の終わりには、ShapeUp は意味のある3Dモデルを作成し
.obj ファイルとしてエクスポートできるようになっていた
- 複数のプラットフォームで動作し、ファイルの読み込みと保存もサポートした
- コード規模は C 2024行 と GLSL 250行 である
- 約2300行である程度実用的な3Dモデラーを作れた点が目を引く成果だった
- プロジェクト自体は比較的シンプルだが、何を作るかを選ぶセンス、作れるだけの知識、1週間で終わらせる規律が重要に作用した
1件のコメント
Hacker News の意見
Raylib の限界について、筆者とまったく同じ考えです。いま Raylib で始めたタワーディフェンス風ゲームを作っているのですが、同じ限界やそれ以外の問題にも多く直面しています
たとえば、プラットフォームごとにフルスクリーン切り替えが一貫して動作しない、画面モードを列挙できない、実行中にレンダリング機能をトグルしにくい、コンパイル済みシェーダーを保存する問題がある、などです
それでも Ray がこのライブラリに注いだ労力には感謝していますし、今後も支援するつもりです。Raylib は素早くプロトタイプを作るには素晴らしいですが、かなり厳しい制約を受け入れない限り、それ以上は簡単ではありません
確かに学びはありましたが、今さら Raylib 関連コードをすべて SDL のようなものに置き換えるには、開発が進みすぎています
Things Unexpectedly Named After People: https://notes.rolandcrosby.com/posts/unexpectedly-eponymous/
最近の戦略は、単にボーダーレスウィンドウモードを使い、本当のフルスクリーンは存在しないふりをすることです
今いちばん大きな問題はフォント処理とテキストレンダリングです。TTF フォントの代わりに事前に焼き込んだビットマップフォントへ切り替える必要がありそうですが、後でローカライズするときにかなりつらそうです
Love2D から移ってきて一番恋しい機能は、複数色のテキストを簡単にレンダリングすることと、テクスチャを簡単に切り出してリピートやタイル表示することです。Raylib では色マークアップを基準にテキストを自分で分割し、幅のオフセットを適用したうえで、改行まで考慮して各断片ごとに描画関数を呼ばなければなりません
画面にテキストを大量に描くと FPS も大きく落ちるようで、テキストの描画呼び出しのバッチングが壊れているのかもしれません。以前はタイルテクスチャ描画関数がありましたが、何らかの理由で削除されました
Wasm とブラウザの 3D/2D グラフィックスでいつも気になるのは、スクロールのような小さな問題がよく見えることです。ここの “Background scrolling & parallax” の例を見ればわかります: https://www.raylib.com/examples.html
複数のデバイスで試しましたが、自分の目がおかしいのでなければ、明らかに滑らかなスクロールではありません。2024年に 2D の滑らかなスクロールがまだ解決済みの問題ではないというのは、どういうことかと思います
「Shape を静的に割り当てた配列に保持する。割り当て失敗もなく、リークもなく、余計なものもない。愛らしい。Shape 100個という制限は、実際には制約ではなかった。レンダラー最適化の時間がほとんどなかったので、100個に到達する前にフレームレートが落ちていただろう」
最近見た中で、早すぎる最適化の回避の最も良い例です
本当に興味深い記事で、メモリの扱い方や raylib で直面した問題など、さまざまな判断について語ってくれてよかったです。ちょうど Crafting Interpreters の第2部に入りながら C を復習しているところなので、C が得意なことを改めて思い出せてよかったです
動画内のリアルタイムデモが本当に良いです。アプリを作るのはさておき、自分が試したら、その動画すら1週間以内には作れなかったと思います
昔、卓上電話機向けの OS に取り組んだことがあります。RAM が 64K しかなかったので動的メモリ管理はまったくなく、静的変数を多用してコンパイラにコンパイル時点ですべて配置させていました
多くのアプリケーションには、動的メモリ管理がそもそも必要ないかもしれない、という点は忘れがちです。固定サイズのバッファをいくつか割り当てておき、そのバッファが埋まったときの例外状況だけをきれいに処理しても十分な場合が多いです
そういう文脈では、C は実際にはずっと安全です。メモリリークがなく、心配すべきなのはバッファオーバーフローだけです。すべての変数が静的に割り当てられているなら、
sizeofを慎重に使うやり方で管理できます今どき Rust や Go が優れた選択肢ではないという意味ではありませんが、素朴な昔ながらの C も今なおよく機能し、悪夢のように複雑である必要はありません
話題から少し外れますが、テキストがぼやけて見えない WebAssembly インターフェースを初めて見られてうれしいです。本当に初めてです
プログラムや一部の OS、たとえば Windows にまで範囲を広げると、ここ数年でテキストのラスタライズ方式が共通の流行でありデフォルト設定にもなり、全体的な問題が生じています
残念ながら、ユーザーは鮮明なテキストを得るためにアンチエイリアスを切れない場合が多く、まれにオプションがあっても、メニューのようなインターフェースには依然としてアンチエイリアスが適用されます
こういうプロジェクトは本当に好きです。今でも C の低レベル性が好きです。今は Rust と Elixir/Erlang をよく使っていますが、C の単純さと明示性がときどき恋しくなります
だから Zig もよく使っています。C の哲学をかなり保ちながら、とても良い形で改善した言語です
Cに対する彼の評価には本当に同意します。「構文が複雑な動作を隠さない。いちいち調べなくてもよいほど単純だ」という部分は特にそうで、さらに言えば、Cについて何か調べる必要があるときも非常に簡単で有益です
単純で古い言語には、それ相応の利点があります
各 Shape を
mallocで個別に割り当て、そのポインタを動的配列に保存すれば、確かに自分で難しくしてしまうことはできます。C# のような言語を使うとそうした割り当て構造が強制される、というような話がありますが、筆者が C でやったように 構造体の固定配列を C# で使うことを、何が妨げているのか気になります誰かがこのプロジェクトを継続してくれるといいですね。あと数か月磨き込めば、特定用途では Blender や FreeCAD の有力な代替になり得ますし、学習曲線もはるかに緩やかに見えます
ただし根本的に SDF で動作するため、三角形や頂点などを使う従来のメッシュとは、モデリング体験も保存するデータも異なります
SDF をメッシュに変換することはマーチングキューブのような方法で可能ですが、そうしたデータはその後 Blender 系のアプリで結局クリーンアップが必要になる可能性が高いです
レンダラーも SDF ベースなら、SDF は素晴らしいものです。たいていはそうではありませんが
すでにご存じでしたらすみません