backend/canvas の設計
設計目標
Canvas バックエンドは、ターミナルバックエンドと同じ DrawList をブラウザで、ピクセル解像度かつカラーで、正確な遮蔽とともに表示します。2 つ目のラスタライザを作るのではなくフロントエンドのソフトウェア深度バッファを再利用し、フレームあたりの DOM 呼び出し回数を少なく保つべきです。MoonBit から Canvas API への呼び出しはどれも JavaScript の境界をまたぐからです。
数学的背景
輝度から色へ
覆われた各ピクセルは、深度テスト済みの LumaBuffer から輝度 を得ます。バックエンドはそれを 段階の濃淡のいずれかに量子化し、前景色 をその段階で拡大縮小します。
TUI のランプと同様に、輝度の量子化誤差は最大 です。既定の では 8 ビットチャネルの分解能より小さいので、結果は見た目には連続です。拡大縮小は格納されたチャネル値について線形です。ディスプレイは sRGB の伝達曲線を適用し、発光輝度は格納値のおよそ 2.2 乗で増えるので、中間の輝度は物理的に線形な描画よりも暗く見えます。これは TUI のランプの見た目と一致しており、補正はしません。
背景は濃淡 0 ではなく別の色です。 の覆われたピクセル(光源に背を向けた面)は真っ黒に塗られ、背景と区別できます。覆われているかどうかを決めるのは値ではなく LumaBuffer の深度(LUMA_FAR_DEPTH / 2 未満)です。
ランレングスによる塗りつぶし
ピクセルごとに塗ると 1 ピクセルにつき 1 回の fillRect がかかります。そこでバックエンドは各行を走査し、同じ濃淡 を持つ連続した覆われたピクセルを極大なランにまとめます。
各ランを高さ 1 の矩形として塗ると、覆われたピクセルを 1 つずつ塗ったのとまったく同じ画像になります。ランは互いに交わらず、その和集合はその行の覆われたピクセル全体であり、ランの各ピクセルは自分の濃淡の色 を受け取るからです。矩形の数はランの数で、覆われたピクセル数以下ですが、フラットシェーディングされた面は輝度が 1 つでランが長くなるため、通常ははるかに少なくなります。塗りつぶしスタイルは前のランと濃淡が違うときだけ設定するので、単一の濃淡の大きな面でもスタイル変更は 1 行あたり多くて 1 回です。
遮蔽
可視性はフロントエンドの深度バッファから得られ、その不変条件は frontend の設計で導出しています。各ピクセルには、描画順序に関係なく、そこを覆う最も近い三角形が表示されます。したがって Canvas の出力は、GSAP SVG バックエンドのペインターズアルゴリズムによる並べ替えとは違い、ピクセル単位で正確です。
設計上の判断
フロントエンドのラスタライザを再利用する
Canvas 2D API は自分でポリゴンを塗れますが、深度バッファがないので遮蔽には並べ替えが必要になり、交差する三角形では失敗します。LumaBuffer にラスタライズすれば正確な遮蔽と TUI バックエンドと同じ透視補正された深度が得られます。代償はピクセルごとの処理を MoonBit で行うことですが、640 × 480 ならアニメーションのデモに十分な速さです。
1 つの色と多数の濃淡
輝度で拡大縮小した単一の前景色は、フロントエンドの取り決め(三角形ごとに 1 つのスカラー輝度)を保ち、ターミナル版のモノクロの見た目をカラーで再現します。マテリアルやオブジェクトごとの色には DrawTriangle の拡張が必要です。
ImageData の代わりにランを使う
ImageData バッファに書き込んで putImageData を 1 回呼べば DOM 呼び出しはフレームあたり 1 回で済みますが、型付き配列のバインディングと JavaScript 側でのピクセルごとの書き込みが必要です。ランレングスの fillRect 呼び出しは rabbita/dom が提供する安定した 2D コンテキスト API だけを使い、ランが長いフラットシェーディングのシーンでは安価です。
呼び出しのたびに正規化する
設定は描画呼び出しのたびに内部で正規化し直されます。どのような経路で作られた設定でも(このパッケージ内部のコードによるものを含め)、レンダラにゼロ除算をさせたり無効な CSS の色を出力させたりはできません。
正しさと不変条件
- 設定された領域のすべてのピクセルが塗られます。まず背景で、次に覆われたピクセルをそれぞれの濃淡で塗ります。
- ランで塗った結果は、覆われたピクセルを個別に塗った結果とピクセル単位で一致します。
- 覆われた各ピクセルには最も近い三角形が表示されます(フロントエンドの深度バッファの不変条件)。
- 正規化後、チャネルは常に 、
shade_levelsは常に に収まります。
フレームあたりのコストは、フロントエンドのラスタライズ、ランを求める の走査 1 回、そしてランごとに 1 回の fillRect です。
採用しなかった案
- WebGL。 ラスタライズを GPU に移せますが、シェーダやバッファが必要になり、フロントエンドがすでに MoonBit で実装しているパイプラインを重複させることになります。
- 深度順に並べたポリゴンの塗りつぶし。 呼び出しは減りますが、交差する三角形や循環的に重なる三角形ではペインターズアルゴリズムによる並べ替えが誤ります。SVG バックエンドはこのトレードオフを受け入れ、Canvas バックエンドは受け入れません。
- ガンマ補正したシェーディング。 このリポジトリの基準となる画であるターミナル出力と見た目が変わってしまいます。
境界
Canvas バックエンドは次のことをしません。
js以外のターゲットで動くこと、DOM なしで動くこと。- アニメーションフレームの予約、入力の読み取り、要素の検索(これらは
demo_canvasパッケージが行います)。 - オブジェクトごとの色、テクスチャ、透明度、アンチエイリアス、GPU アクセラレーションへの対応。
- 呼び出し側がパッケージの外から色や濃淡の段階数を変えること。
CanvasRenderConfigのフィールドはそこでは読み取り専用で、作れるのはdefaultとsizedだけです。 - アスペクト比の補正(キャンバスのピクセルは正方形です)。