
2026/09/25 19:08
Brainfuck でレイ・トラサーを書いた
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
著者は、CMakeを再学習し、複雑なツールを直接構築することへの助言を読み、その上で、わずか 8 つの操作しかない Brainfuck という言語で完全に高度な光线追跡アルゴリズムを実装することを意図しました。この極限的な挑戦を選んだのは、既存の簡略化やネイティブ命令を用いず、第一原理から特定のハイレゾリューション画像を再現するためです。正確な計算をサポートするために、実装では符号付き Q16.16 データ形式(半径 1000 の球に対しては Q8.8 は粗すぎた)を使用し、乗算には反復加算によるシミュレーション、除算には 256 倍の手動の長除シフトを実装しました。コード論理は SSA に類似するイテレーティブ形式に変換され、ハンガリアンスタイルの可変名が使用され、解析と生成はカスタム中間 DSL を通じて分離されました。この DSL は abs、add、div、sqrt などの操作を処理しました。平方根はスケーリングされた値に対する整数平方根(isqrt(2^16 * N))として実装され、0.305 未満での不適切なテイラー級数適合を回避しました。スーパーサンプリングアンチエイリアシングのためのランダム性は、期間が 256 の線形合同生成器の変種を使用しました。ブール論理は符号の管理のためにオフセットを加算し、真偽値判定には否定演算やビット演算を用いました。最終的なプログラムサイズは約 23MB に達し、出力画像は 0.9MB で、約 1 ピクセル分毎分の速度(≈100 本の線計算/分)でした。初期の見積もりでは標準的なラップトップで約 62.5 日と予測されましたが、実際の実行時間は空と球の跳ね返りにより更长くなりました。Reddit のコメントに促されて JIT インタプリタの改善が行われ、レンダリング速度が劇的に向上しました。低レベルシミュレーションにおけるわずかな精度誤差が独特なテクスチャを生み出し、ファン・ゴッホ様式を思わせるものとなりました。
本文
CMake ランゲージを用いた BF 言語によるレイトレーサーの実装と考察
C++ のシステムプログラミングコンテスト準備中に、CMake の再学習を機に発見した「汎用言語でツールを作成し、ビルドプロセスに統合する」というプラクティスをきっかけに、CMake ランゲージを用いて BF 言語(Brachylog / Brainfuck)によってレイトレーサーを実装したプロジェクトの記録です。
CMake ランゲージの基礎と BF の特徴
- 言語の選定理由
- レイトレーサーの実装において、複雑な API に依存するコードから脱却することを目的として、最もシンプルな言語である BF を選択しました。
- 言語が単純であるため、コードベースも非常にシンプルになり(通常数行)、Muller 氏のコメントに対する反論例を示すことができました。
- BF の基本的な仕組み
- データ構造: 片方向無限セルテープ(1 セルに
を格納)。u8 - ポインタ制御:
: ポインタを右へ移動。>
: ポインタを左へ移動。<
- 入出力:
: 入力バイトを読み込み、現在のセルに格納。,
: 現在のセルの値を出力(印刷)。.
- 変数操作:
: 現在のセルをインクリメント。+
: 現在のセルをデクリメント。-
- 制御構造:
と[
: ループ構文(タリング完全性を実現)。]
時点:セル値が 0 でなければループ処理、0 であればスキップ。[
時点:セル値が非ゼロであれば戻る、ゼロであれば終了。]
- データ構造: 片方向無限セルテープ(1 セルに
事前検討と設計方針
レンダリング目標
- RIW の Metal セクションでレンダリングされた正確な画像を出力することを目指しました。
- C コード自体の複雑性を避けるため、C パーサーの記述はスコープ外と判断し、LLM に任せる方針をとりました。
データ型設計 (Q フォーマット)
- 実装方針: すべての数値(
,double
など)を BF のセル組み合わせで表現。bool - 固定小数点: ビットの半分を分数部、残りを整数部とする(後述の Q8.8/Q16.16)。
- フォーマット選定:
- 初期案:Q8.8(解像度
、範囲1/256
)。不適切。球体半径[-128, 128)
の表現が不可能。r=1000 - 採用案:有符号 Q16.16。
- 解像度:
1/2^16 - 範囲:
[-2^15, 2^15)
- 解像度:
- 初期案:Q8.8(解像度
SSA 形式と DSL の導入
- SSA(単一静的アサインメント)風変換: 再帰的コードの反復化と、ヒュンガリアン式表記による衝突回避。
- DSL(ドメイン特化言語):
- 解析とコード生成を分離するため、中間表現として使用。
- 操作例:
,abs
,add
,mul
,sqrt
,while
など。func
数値演算の手法選定
- 平方根 (
) の実装:sqrt- ヒルソンの公式:除算を伴うため不適切。
- 反復減法:コード量は少ないが相対的に高価。
- テイラー級数(
の領域で精度不足)。x <= 0.305 - 採用: 長除法。
- エンコード値
よりN = x * 2^16
を計算し、精度劣化が許容範囲 (isqrt(N)
) であることを確認。< 1/2^16
- エンコード値
- 確率的サンプリング:
- 完全なシーケンス(周期 256 値)を採用し、スーパーサンプリングによるアンチエイリアシングに使用。
実装詳細とアルゴリズム
プリミティブ操作
1. 移動 (Move)
データを左のセルから右のセルへ転送する(初期値ゼロ化を含む)。
[ # ループ開始 - # デクリメント(送信元) >+ # 右へ移動しインクリメント(宛先) < # 戻り(ループ制御用) ]
2. コピー (Copy)
配列
[a, 0, 0] を [0, a, a] に変換する処理。必要に応じて末尾の a を内部に移動可能。
3. 乗算 (Multiply)
- 単一セル同士の乗算: 反復加算方式。
- 一時変数として
を使用。[a, b, a->0, b->0]
の値分だけb
をコピーし、結果に累積。a
- 一時変数として
- 複数セルの乗算 (Convolution):
- 2 セル入力を 8 セルの結果配列へマッピング。
- 式:
result[i+j] += a_i * b_j - オーバーフロー処理により、下位 2 セルは破棄される。
4. 除算 (Divide)
- 手動長除法の BF 実装:
- 被除数を最高位から読み込み、逐次剰余を計算 (
)。R = R * 256 + A[next] - 除数
より大きい場合、反復減算と結果のインクリメントを行う。D -
while R >= D: R -= D result += 1
- 被除数を最高位から読み込み、逐次剰余を計算 (
5. 比較 (Compare)
- 両方のセルを同時にデクリメントし、ゼロになるまでの差異を検出。
- 符号付き値の扱い: 最高位ビット(0x80)に基づき正負を判定。オーバーフローさせることで単一範囲 (
) で表現。00..ff
: 負の半分 → 加算で正へシフト80..ff
: 正の半分00..7f
6. 論理演算
- 真偽値: 最低ビットがセット(真)または全ビットゼロ(偽)。
- 実装方法: ビット表現に基づく操作。
:not
のトリックを使用。-x = ~x + 1
: 最高ビットチェックにより符号反転を制御。abs
7. コントロールフロー
- 関数 (Func): SSA スタイルで、関本体名とコードを関連付け、
でインライン展開。call func - 分岐 (If/Else):
: 条件を満たせば処理、否则スキップ。if
: フラグで制御され、条件計算後に移動する構造をとる。else
アーティファクトと性能測定
プロジェクト規模
- 総サイズ: 最終的に 23 MB に達した。
- 画像自体(約 0.9 MB)よりも大規模であり、圧縮効率は低かった。
- 計算速度: 約 1 分間に 100 のレイ計算(1 ピクセルあたり)。
- 想定時間:400x225 ピクセルで 62.5 日。
- ボトルネック: 球体とのバウンシング(反射)による ETA の大幅な遅延。
精度と視覚的差異
- C コードレンダリングとの比較:
- 90,000 ピクセル中、1,229 ピクセルのみが異なる(主に値
の差)。1 - 全体的に C コードの近似として機能。
- 90,000 ピクセル中、1,229 ピクセルのみが異なる(主に値
最適化の可能性と制約
- 地面の粗さ: タッチされたセル数を減らせるが、精度低下を伴う。
- 正規化スキップ: ランダムベクトルの正規化ステップを省略可能だが、散乱分布が変更され正確なビットコード再現性が失われる。
更新と結論
Reddit スレッドでの議論(
fork/join プリミティブの導入)を受け、JIT インタプリターを改善する作業が行われました。
- パフォーマンス: JIT による実装により劇的な性能向上が実現されました。
- 視覚的特徴: 実際のレンダリングはヴァン・ゴッホの絵のように少しぼやけており、これは精度誤差によるものと推察されます。