backend/tui API
パッケージ Luna-Flow/geometry3d/backend/tui は、フロントエンドの DrawList をテキストとして描きます。深度バッファ付きの文字フレームバッファ、輝度を文字に対応付けるシェードランプ、背景パターン、縦長のターミナルセルの補正、そして単一画像(.tuiimg)とアニメーションシーケンス(.tui3d)のためのプレーンテキスト形式を備えます。すべてのターゲットで動作して文字列を生成し、その出力は呼び出し側に任されます。
import {
"Luna-Flow/geometry3d/view",
"Luna-Flow/geometry3d/frontend",
"Luna-Flow/geometry3d/backend/tui",
}
アスペクト補正と量子化は TUI の設計で説明し、TUI チュートリアルではシーンを段階的に描画します。
設定
DEFAULT_WIDTH, DEFAULT_HEIGHT
既定の出力サイズで、80 列 × 32 行です。
pub const DEFAULT_WIDTH : Int = 80
pub const DEFAULT_HEIGHT : Int = 32
DEFAULT_TERMINAL_Y_SCALE
既定の縦方向の圧縮率 0.5 で、高さが幅のおよそ 2 倍のターミナルセル向けです。
pub const DEFAULT_TERMINAL_Y_SCALE : Double = 0.5
DEFAULT_SHADE_RAMP
既定のシェードランプ " .:-=+*#%@" で、最も暗いものから最も明るいものまで 10 文字です。
pub const DEFAULT_SHADE_RAMP : String = " .:-=+*#%@"
TuiRenderConfig
TuiRenderConfig は、出力サイズ、縦方向の圧縮率、シェードランプ、背景パターンをまとめたものです。
pub struct TuiRenderConfig {
width : Int
height : Int
terminal_y_scale : Double
shade_ramp : String
background_pattern : (Int, Int, Int, Int) -> Char
}
フィールドはパッケージ外からは読み取り専用です。設定は下の 2 つのコンストラクタから得てください。別のランプや背景で描くには、チュートリアルにあるように FrameBuffer、shade_char、draw_triangle_z を直接使います。
TuiRenderConfig::default, TuiRenderConfig::sized
TuiRenderConfig::default() は 80 × 32 で、既定の圧縮率、ランプ、dotted_background を使います。TuiRenderConfig::sized(w, h) は同じ設定を指定したサイズで作り、正でない寸法は既定値に置き換えます。
pub fn TuiRenderConfig::default() -> Self
pub fn TuiRenderConfig::sized(Int, Int) -> Self
default は通常のコンストラクタであり、Default トレイトの実装ではありません。
描画
render_draw_list
render_draw_list(list, config) は背景パターンで埋めたフレームバッファを作り、リストの各三角形を描きます。頂点を縦方向に圧縮し、三角形の輝度に対応する文字を選び、深度テスト付きでラスタライズします。
pub fn render_draw_list(@frontend.DrawList, TuiRenderConfig) -> FrameBuffer
描画リストは設定と同じサイズのビューポートに投影されている必要があります。輝度 0 の三角形はランプの最初の文字(既定では空白)で描かれるので、照らされていない面も背後のものを隠します。
render_frame
render_frame(list, config) は render_draw_list(list, config).to_string() と同じです。
pub fn render_frame(@frontend.DrawList, TuiRenderConfig) -> String
render_scene
render_scene(scene, view, config) は build_draw_list と render_draw_list を 1 回の呼び出しで実行します。
pub fn render_scene(@frontend.Scene, @frontend.RenderView, TuiRenderConfig) -> FrameBuffer
test "render a scene" {
let config = @tui.TuiRenderConfig::sized(20, 8)
let scene = @frontend.Scene::single(
@core.cube_mesh(1.0),
@core.Transform3::rotation(0.5, 0.7, 0.0),
@frontend.Light::default(),
)
let view = @frontend.RenderView::perspective(
@view.Camera3::default(4.0),
@view.PerspectiveProjection::new(@view.Viewport::new(20, 8), 10.0),
)
let frame = @tui.render_scene(scene, view, config).to_string()
inspect(
frame,
content=(
#|....................
#|....................
#|........===##.......
#|.......===####......
#|......====###.......
#|.......... #.......
#|....................
#|....................
#|
),
)
}
draw_list_to_tui_luma
draw_list_to_tui_luma(list, config) は同じ縦方向の圧縮を適用しながら、リストを設定サイズのフロントエンドの LumaBuffer にラスタライズします。文字に変換する前に露光を積算するのに使います。
pub fn draw_list_to_tui_luma(@frontend.DrawList, TuiRenderConfig) -> @frontend.LumaBuffer
render_luma_buffer
render_luma_buffer(buffer, config) は輝度バッファをフレームバッファに変換します。値が 0.0 より大きい各ピクセルには値に対応するランプ文字が入り、深度も保持されます。それ以外のピクセルには背景が表示されます。
pub fn render_luma_buffer(@frontend.LumaBuffer, TuiRenderConfig) -> FrameBuffer
バッファと設定サイズが重なる部分だけが使われます。
render_luma_frame
render_luma_frame(buffer, config) は render_luma_buffer(buffer, config).to_string() と同じです。
pub fn render_luma_frame(@frontend.LumaBuffer, TuiRenderConfig) -> String
test "luma path" {
let config = @tui.TuiRenderConfig::sized(10, 4)
let list = @frontend.DrawList::new()
list.push_triangle(
@frontend.DrawTriangle::new(
@view.ProjectedVertex::new(0.0, 0.0, 1.0),
@view.ProjectedVertex::new(10.0, 0.0, 1.0),
@view.ProjectedVertex::new(0.0, 8.0, 1.0),
1.0,
),
)
let luma = @tui.draw_list_to_tui_luma(list, config)
inspect(
@tui.render_luma_frame(luma, config),
content=(
#|..........
#|@@@@@@@@@.
#|@@@@@@....
#|@@@@......
#|
),
)
}
フレームバッファ
FAR_DEPTH
FAR_DEPTH は背景セルの深度 です。
pub const FAR_DEPTH : Double = 1.0e30
FrameBuffer
FrameBuffer は行優先で並んだ文字のグリッドで、セルごとに深度を持ちます。
pub struct FrameBuffer {
width : Int
height : Int
cells : Array[Char]
depths : Array[Double]
}
FrameBuffer::new
FrameBuffer::new(width, height, pattern) は各セル を深度 FAR_DEPTH で pattern(x, y, width, height) により埋めます。
pub fn FrameBuffer::new(Int, Int, (Int, Int, Int, Int) -> Char) -> Self
FrameBuffer::index
buffer.index(x, y) は境界チェックなしで y * width + x を返します。
pub fn FrameBuffer::index(Self, Int, Int) -> Int
FrameBuffer::set_pixel_if_closer
buffer.set_pixel_if_closer(x, y, depth, ch) は depth + DEPTH_EPSILON が格納済みの深度より小さいときに ch を書き込みます。バッファ外への書き込みは無視されます。
pub fn FrameBuffer::set_pixel_if_closer(Self, Int, Int, Double, Char) -> Unit
FrameBuffer::to_string
buffer.to_string() は各行の後に '\n' を付けて連結したものを返し、その長さは height * (width + 1) 文字です。
pub fn FrameBuffer::to_string(Self) -> String
これはメソッドであり、Show の実装ではありません。
test "frame buffer" {
let buffer = @tui.FrameBuffer::new(4, 2, @tui.blank_background)
buffer.set_pixel_if_closer(1, 0, 5.0, 'a')
buffer.set_pixel_if_closer(1, 0, 7.0, 'b') // farther: ignored
buffer.set_pixel_if_closer(9, 9, 1.0, 'c') // outside: ignored
inspect(buffer.to_string(), content=" a \n \n")
}
背景
dotted_background, blank_background, checker_background
これらのパターンは各セルを '.' で、' ' で、あるいは が偶数か奇数かに応じて '.' と ' ' を交互に埋めます。
pub fn dotted_background(Int, Int, Int, Int) -> Char
pub fn blank_background(Int, Int, Int, Int) -> Char
pub fn checker_background(Int, Int, Int, Int) -> Char
パターンは (x, y, width, height) -> Char 型の任意の関数なので、自分で書くこともできます。
test "backgrounds" {
inspect(@tui.FrameBuffer::new(4, 2, @tui.checker_background).to_string(), content=". . \n . .\n")
let border = fn(x : Int, y : Int, w : Int, h : Int) -> Char {
if x == 0 || y == 0 || x == w - 1 || y == h - 1 { '#' } else { ' ' }
}
inspect(@tui.FrameBuffer::new(4, 3, border).to_string(), content="####\n# #\n####\n")
}
ラスタライズ用の補助関数
apply_terminal_y_scale
apply_terminal_y_scale(p, height, k) は投影された頂点を中央の行に向けて圧縮します。 で、x と depth は変わりません。
pub fn apply_terminal_y_scale(@view.ProjectedVertex, Int, Double) -> @view.ProjectedVertex
clamp01
clamp01(v) は数値を にクランプします。
pub fn clamp01(Double) -> Double
shade_char
shade_char(ramp, intensity) は、 文字のランプのうちインデックス の文字を返します。
pub fn shade_char(String, Double) -> Char
ランプは空であってはならず、基本多言語面の文字(それぞれ UTF-16 の 1 単位)で構成する必要があります。
edge_function
edge_function(ax, ay, bx, by, px, py) は を返します。これは符号付きの面積の 2 倍で、点 が直線 のどちら側にあるかを決めます。
pub fn edge_function(Double, Double, Double, Double, Double, Double) -> Double
draw_triangle_z
draw_triangle_z(buffer, p0, p1, p2, ch) は 1 つの三角形を文字 ch でフレームバッファにラスタライズします。セルの中心でサンプリングし、深度テストには透視補正された深度を使います。
pub fn draw_triangle_z(FrameBuffer, @view.ProjectedVertex, @view.ProjectedVertex, @view.ProjectedVertex, Char) -> Unit
どちらの巡回順も受け付けます。面積が DEPTH_EPSILON 以下の三角形はスキップされます。頂点は縦方向の圧縮なしでそのまま使われます。
test "rasterization helpers" {
inspect(@tui.shade_char(@tui.DEFAULT_SHADE_RAMP, 0.0), content=" ")
inspect(@tui.shade_char(@tui.DEFAULT_SHADE_RAMP, 0.5), content="+")
inspect(@tui.shade_char("ab", 2.0), content="b")
let p = @tui.apply_terminal_y_scale(@view.ProjectedVertex::new(3.0, 20.0, 1.0), 20, 0.5)
inspect(p.y, content="15")
inspect(@tui.edge_function(0.0, 0.0, 1.0, 0.0, 0.0, 1.0), content="-1")
let buffer = @tui.FrameBuffer::new(4, 2, @tui.dotted_background)
@tui.draw_triangle_z(
buffer,
@view.ProjectedVertex::new(0.0, 0.0, 1.0),
@view.ProjectedVertex::new(4.0, 0.0, 1.0),
@view.ProjectedVertex::new(0.0, 2.0, 1.0),
'#',
)
inspect(buffer.to_string(), content="###.\n#...\n")
}
画像とシーケンス
TuiImage
TuiImage は描画済みの 1 フレームとそのサイズです。
pub struct TuiImage {
width : Int
height : Int
content : String
}
TuiImage::new
TuiImage::new(width, height, content) は画像を作ります。1 未満の寸法は 1 に引き上げられます。
pub fn TuiImage::new(Int, Int, String) -> Self
encode_tui_image, decode_tui_image
encode_tui_image(image) は .tuiimg テキスト形式を書き出し、decode_tui_image(text) はそれを読み戻します。
pub fn encode_tui_image(TuiImage) -> String
pub fn decode_tui_image(String) -> TuiImage
形式は、GEOMETRY3D_TUI_IMAGE v1 の行、width=W と height=H の行、---image--- の行、そして改行で終わる内容からなります。デコーダは内容を最大 H 行読み、'\r' を取り除きます。失敗することはなく、読めない数値は既定値に、マジック行がなければ空の 80 × 32 画像になります。
TuiFrame
TuiFrame はシーケンスの 1 フレームです。
pub struct TuiFrame {
content : String
}
TuiFrame::new
TuiFrame::new(content) はフレームを包みます。
pub fn TuiFrame::new(String) -> Self
TuiSequence
TuiSequence はアニメーションで、サイズ、フレームレート、フレームからなります。
pub struct TuiSequence {
width : Int
height : Int
fps : Int
frames : Array[TuiFrame]
}
TuiSequence::new, TuiSequence::push_frame
TuiSequence::new(width, height, fps) は空のシーケンスを作り、1 未満の値は 1 に引き上げます。sequence.push_frame(content) はその場でフレームを追加します。
pub fn TuiSequence::new(Int, Int, Int) -> Self
pub fn TuiSequence::push_frame(Self, String) -> Unit
encode_tui_sequence, decode_tui_sequence
encode_tui_sequence(sequence) は .tui3d テキスト形式を書き出し、decode_tui_sequence(text) はそれを読み戻します。
pub fn encode_tui_sequence(TuiSequence) -> String
pub fn decode_tui_sequence(String) -> TuiSequence
形式は、GEOMETRY3D_TUI_SEQUENCE v1 の行、width=W、height=H、fps=F、frames=N の行、そしてフレームごとに ---frame--- の行とその内容および改行からなります。デコーダは各マーカーの後の最大 H 行を 1 フレームとし、frames= の数は無視します。マジック行がなければ 30 fps の空の 80 × 32 シーケンスになります。
test "files" {
let sequence = @tui.TuiSequence::new(3, 1, 12)
sequence.push_frame("abc\n")
sequence.push_frame("def\n")
let text = @tui.encode_tui_sequence(sequence)
inspect(
text,
content=(
#|GEOMETRY3D_TUI_SEQUENCE v1
#|width=3
#|height=1
#|fps=12
#|frames=2
#|---frame---
#|abc
#|---frame---
#|def
#|
),
)
let back = @tui.decode_tui_sequence(text)
inspect(back.frames[1].content, content="def\n")
let image = @tui.decode_tui_image(@tui.encode_tui_image(@tui.TuiImage::new(3, 1, "xyz")))
inspect(image.content, content="xyz\n")
}