backend/tui の設計
設計目標
TUI バックエンドは、フロントエンドの三角形を文字だけでターミナルに表示します。ターミナルのセルは粗く正方形でないピクセルで、このバックエンドでは色もないため、このパッケージの仕事は 3 つです。立方体が立方体に見えるようセルの形を補正すること、連続的な輝度を少数の文字に対応付けること、セルごとに遮蔽を解決することです。テスト、ファイル、動画ツールが扱えるプレーンな文字列を生成し、デモに属する ANSI エスケープコードやターミナルの入出力は含みません。
数学的背景
正方形でないセル
フロントエンドは正方形の単位からなるグリッドに投影します。 の 1 単位と の 1 単位は物理的に同じ長さです。一般的なターミナルのセルは高さが幅のおよそ 2 倍です。投影の をそのまま行番号に使うと、すべての形がセルの縦横比 だけ縦に引き伸ばされて見えます。物理的な長さをセルの幅で測ると、 単位の垂直な線分は 行に収まらなければなりません。apply_terminal_y_scale はこの拡大縮小を中央の行を中心に適用し、画像が中央に保たれるようにします。
と深度は変わりません。この写像は画面座標についてアフィンなので重心座標を保ち、したがって view の設計の透視補正された深度の規則も保ちます。圧縮の後で を補間しても、圧縮の前と同じ深度が得られます。
ターミナルでの画角
ScientificCamera を使うと、投影スケール により垂直方向の画角は 単位に広がり、圧縮によってそれが 行になります。画像は歪みませんが、垂直方向の画角は中央の半分の行を占めます。言い換えると、ターミナルの高さ全体に表示される垂直方向の角度は
となり、 ではありません。デモはこれを踏まえて焦点距離を選んでいます。tools/ の動画書き出しツールは同じ の縦横比でセルを描くので、2 つの変換は物理的に打ち消し合います。
濃淡の量子化
セルに置くインクの量で並べた 文字 のランプは、輝度を等間隔の 段階のうち最も近いものに丸めて表します。
既定のランプ " .:-=+*#%@" は なので、量子化誤差は最大 です。輝度 は最初の文字である空白に対応するので、照らされていない面も描かれ、背後のものを隠します。浮動小数点の丸めで 1 をわずかに超えた輝度はクランプされます。
ラスタライズと深度
draw_triangle_z は frontend の設計で導出したエッジ関数の規則と深度の不変条件を使います。セルの中心 が閉じた三角形の中にあればセルは覆われ、そこの深度は で、格納済みの深度より DEPTH_EPSILON を超えて近いときだけ書き込みます。すべての三角形を描いた後、各セルには描画順序に関係なく、そこを覆う最も近い三角形(ほぼ同点なら最初に描かれたもの)の文字が表示されます。
2 つの描画経路
直接経路(render_draw_list)は三角形ごとに文字を選び、文字をラスタライズします。輝度経路(draw_list_to_tui_luma に続けて render_luma_buffer)は、まず輝度をフロントエンドの LumaBuffer にラスタライズし、その後セルごとに量子化します。単一のフレームでは、照らされた面ではどちらも同じ文字になります。違いは 2 つあります。
- 輝度経路はあらゆる処理の後で量子化します。量子化の前に 回の露光を平均すると、文字を平均しても得られない中間の濃淡が得られます。
- 輝度経路は値が より大きいセルだけを描くので、照らされていない面には空白ではなく背景パターンが表示されます。
ファイル形式
どちらの形式も行指向のテキストです。
GEOMETRY3D_TUI_SEQUENCE v1 GEOMETRY3D_TUI_IMAGE v1
width=W width=W
height=H height=H
fps=F ---image---
frames=N <H lines>
---frame---
<H lines>
---frame---
<H lines>
...
デコーダは '\n' で分割し、'\r' を取り除き、ヘッダの行の = の後の数字を読み(読めなければ既定値)、各マーカーに続く 行を取ります。往復の性質:各フレームの内容がちょうど 行からなり、各行が '\n' で終わり '\r' を含まないなら、decode(encode(s)) は s と同じサイズ、フレームレート、フレームの内容を持ちます。エンコーダはヘッダを書き、各マーカーとそれに続く 行をそのまま書き、デコーダは各マーカーの後のちょうど 行を読み戻して行末を付け直すからです。frames= の数は参考情報で、デコーダは代わりにマーカーを数えます。そのため --record-stdout はフレーム数を前もって知らなくてもフレームをストリーミングできます。
設計上の判断
アスペクト補正はバックエンドで行う
正方形でないセルは出力デバイスの性質であって、シーンやカメラの性質ではないので、補正はここにだけ置きます。core、view、frontend は正方形の単位を保ち、ピクセルが正方形の Canvas と SVG のバックエンドは補正を必要としません。係数は設定のフィールドなので、パッケージはほかの形のセルを持つターミナルにも対応できます。
三角形ごとに 1 文字
各三角形の輝度は一様なので、直接経路は文字を一度だけ選んで文字をラスタライズします。フラットシェーディングでは、これが最も安価で正しい選択です。輝度経路は、量子化の前に輝度に対する演算が必要な場合のためにあります。
関数としてのパターン
背景はセルの位置とバッファのサイズの関数なので、点、市松模様、枠、グラデーションはどれも 1 行で書け、画像を保持する必要もありません。dotted_background が既定なのは、行末の空白が見えないターミナルでもフレームの範囲がわかるからです。
ターミナル入出力ではなく文字列
このパッケージは String の値を返し、出力は一切しません。画面のクリア、時間計測、COLUMNS/LINES の読み取り、ファイルへの書き込みはデモの仕事です。これによりバックエンドはテスト(壊れやすいフレーム全体のスナップショットではなく振る舞いの検証)やすべての MoonBit ターゲットで使えます。
寛容なプレーンテキストの形式
これらの形式はテキストエディタで読め、差分を取れ、Python の動画書き出しツールで簡単に解析できます。デコーダは失敗せず既定値に戻ります。唯一の利用者であるデモのプレーヤーにとっては、中断するより何かを表示するほうが望ましいからです。
正しさと不変条件
FrameBuffer::to_stringの長さはちょうどheight * (width + 1)文字です。shade_charはどんな入力にもランプの文字を返し、 での誤差は最大 です。- 縦方向の圧縮は透視補正された深度を保つので、圧縮の有無で遮蔽は変わりません。
- 各セルにはそこを覆う最も近い三角形が表示され、背景のセルは深度
FAR_DEPTHのままです。 - 上で述べた条件のもとで、シーケンスと画像は往復変換で元に戻ります。
三角形を描くコストは、そのバウンディングボックスのセル単位の面積に比例します。ボックスはバッファに切り詰められないので、画面外の巨大な三角形(視点の近くの幾何から生じる)は何も書き込まなくても時間がかかります。
採用しなかった案
- ANSI カラーや Unicode のブロック文字、点字。 解像度や色は増えますが、ターミナルの機能やフォントに依存します。純粋な ASCII のランプは、ファイルや動画書き出しツールを含めどこでも動きます。
- 投影でアスペクト比を補正する。 一様でない投影スケールは、デバイスの性質を
viewやほかのすべてのバックエンドに漏らします。 - ディザリング。 誤差拡散は平らな面に模様を加えてしまいます。10 段階あれば、面ごとのシェーディングはすでに十分読み取れます。
- バイナリや圧縮されたファイル形式。 テキストならファイルを調べやすく、ツールも単純に保てます。必要なら記録は汎用のツールでよく圧縮できます。
境界
TUI バックエンドは次のことをしません。
- 出力、画面のクリア、ターミナルのサイズや環境変数の読み取り、時間計測。
- ANSI エスケープコード、色、Unicode のブロック文字の出力。
- 三角形のバッファへのクリッピング、アンチエイリアス、ディザリング。
- ファイルの内容が宣言された幅と一致するかの検証。
- 呼び出し側がパッケージの外から
TuiRenderConfigを通してランプ、背景、圧縮率を変えること(フィールドは読み取り専用です)。これらの用途は下位の関数で賄えます。