demo の設計

設計目標

ターミナルデモは、ライブラリのパッケージがごくわずかな糊付けで動くレンダラになることを示します。シーン、カメラ、build_draw_list、バックエンドがあれば十分です。また、テストでは目で判断できない部分(透視、照明、影、ドリーズーム、露光効果)を動かして確かめ、動画に変換できる記録を作ります。オペレーティングシステムに触れるもの、つまり引数の解析、環境変数、時計、ターミナル、ファイルはすべてここにあります。

数学的背景

ドリーズーム

科学カメラでは、視線軸に沿って距離 dd にある高さ YY の物体は(ターミナルの圧縮前に)s Y/ds\,Y/d 行の高さに見えます。ここで s=Hf/hs = H f / h です(view の設計を参照)。カメラが動く間も被写体の大きさを一定に保つには、

f(d)d=f0d0⟹f(d)=f0 dd0.\frac{f(d)}{d} = \frac{f_0}{d_0} \quad\Longrightarrow\quad f(d) = f_0 \, \frac{d}{d_0} .

各シーンは次の値を使います。

シーン距離 dd基準 (d0,f0)(d_0, f_0)焦点距離の範囲
--hitchcockフレーム kk について 5.7+2.3sin⁡(0.035 k)5.7 + 2.3 \sin(0.035\,k)、範囲は [3.4,8][3.4, 8](4.5,18 mm)(4.5, 18\ \text{mm})13.6–32 mm
--dolly3.2+66.37⋅12(1+sin⁡(0.028 k))3.2 + 66.37 \cdot \tfrac12\big(1 + \sin(0.028\,k)\big)、範囲は [3.2,69.57][3.2, 69.57](3.2,23 mm)(3.2, 23\ \text{mm})23–500 mm

ドリーのシーンの最遠距離 69.565…=3.2⋅500/2369.565\ldots = 3.2 \cdot 500 / 23 は、焦点距離の最大値がちょうど 500 mm になるように選んであります。被写体の後ろ、カメラから距離 d+Δd + \Delta にある物体は、基準の見え方に対して

s/(d+Δ)s0/(d0+Δ)=d (d0+Δ)d0 (d+Δ)\frac{s/(d + \Delta)}{s_0/(d_0 + \Delta)} = \frac{d\,(d_0 + \Delta)}{d_0\,(d + \Delta)}

倍に拡大縮小されて見えます。この係数は d→∞d \to \infty で (d0+Δ)/d0(d_0 + \Delta)/d_0 に近づきます。立方体の約 Δ≈5\Delta \approx 5 単位後ろにある円盤の壁は、立方体が固定されたまま基準の大きさの約 2.62.6 倍に膨らみます。望遠側のカメラは正射影に近づくので、背景が平板に見えます。テストで、被写体の投影サイズの比がフレーム間で一定であることを確かめています。

露光

露光モードは、シャッター区間にわたる画像の時間積分(frontend の設計を参照)を N=12N = 12 個のサンプルで近似します。

Lˉ(x)≈112∑j=011L(x,t−(11−j) δ),\bar L(x) \approx \frac{1}{12} \sum_{j=0}^{11} L\big(x, t - (11 - j)\,\delta\big),

ここで δ\delta はアニメーションの 1 ステップ(ドリーのシーンでは 1 フレーム、メッシュのシーンでは回転の 1 増分)です。--flow-exposure では、各サンプル LjL_j をまず探索半径 4、パッチ半径 1 のブロックマッチングによるフロー dj=flow(Lj,Lcurrent)d_j = \text{flow}(L_j, L_{\text{current}}) で現在のフレームにワープします。ExposureSettings::auto も参照しますが、常に 1 サンプルを返すので、サンプル数はデモ自身の定数 12 で決まります。

設計上の判断

システムへのアクセスを 1 つのパッケージに集める

COLUMNS と LINES の読み取り、フレームの時間計測、ANSI エスケープによる画面のクリア、moonbitlang/x/fs によるファイル入出力は、このリポジトリで唯一の移植性のない操作で、すべて src/demo/main.mbt にあります。ライブラリのパッケージは純粋でターゲットに依存しません。デモはシェルのプロンプト用に 1 行空けるので、フレーム全体を出力してもスクロールしません。

小さく寛容なコマンドライン

オプションは完全一致で判定される単純なフラグで、優先順位は固定され、黙って既定値に戻ります。これでパーサは数行で済み、コマンドラインからデモをクラッシュさせることは難しくなりますが、その代わり入力ミスは報告されません。同じ理由で、数値のオプションは数字だけを残します。

ビジーウェイトによるフレームの間隔制御

ライブループは @env.now() を前のフレームの時刻と比べ、33 ms 経つまで空回りします。移植可能な標準ライブラリにはスリープのプリミティブがありませんが、これなら不要で、間隔も安定します。代償として、デモの実行中は CPU コアが 1 つ使われ続けます。

ストリーミング記録

--record はメモリ上に TuiSequence を作って最後にエンコードします。単純で、ライブラリのエンコーダも検証できます。--record-stdout は同じ形式を 1 フレームずつ出力するので、大きなフレームを何千枚も記録してもメモリは 1 フレーム分で足ります。フレーム数はタイムラインからわかるので、形式の frames= ヘッダは最初に書けます。

正しさと不変条件

  • ドリーのシーンの被写体は投影サイズを保ちます。f/df/d は作り方から一定です(テスト済み)。
  • どのフレームも設定された幅と高さを正確に持つので、記録はヘッダのサイズでデコードできます。
  • 記録のフレーム数は Timeline::frame_count() = ⌈duration⋅fps⌉\lceil \mathit{duration} \cdot \mathit{fps} \rceil です。
  • 露光の重みの和は 1 なので、露光フレームはサンプルの輝度の範囲内に収まります。

採用しなかった案

  • 引数解析ライブラリは、十数個のフラグのために依存関係を増やすことになります。
  • ターミナルの raw モードと差分の再描画はちらつきを減らせますが、プラットフォーム固有のターミナル制御が必要です。
  • フレーム間のスリープには各ターゲットで FFI が必要です。

境界

デモは次のことをしません。

  • MoonBit の API の公開、native 以外のターゲットでの実行。
  • 実行中の入力の読み取り、ターミナルに合わせたサイズ変更、設定ファイルの提供。
  • オプションの検証や未知のオプションの報告。
  • カラーやブラウザでの描画(demo_canvas と demo_gsap を参照)。