frontend API
パッケージ Luna-Flow/geometry3d/frontend は、シーンとカメラを、バックエンドに依存しないシェーディング済み・投影済みの三角形のリスト(DrawList)に変換します。また、Canvas バックエンドと露光効果が使うスカラー画像用のソフトウェア深度バッファ(LumaBuffer)、ブロックマッチングによるオプティカルフロー推定、露光設定、アニメーションのタイムラインも持ちます。文字、色、DOM については何も知りません。
import {
"Luna-Flow/geometry3d/core",
"Luna-Flow/geometry3d/view",
"Luna-Flow/geometry3d/frontend",
"Luna-Flow/linear-algebra/mutable" @la,
}
パイプライン、シャドウマップ、ラスタライズの規則は frontend の設計で説明し、frontend チュートリアルで使い方を示しています。
シーン
SceneObject
SceneObject は 1 つのメッシュとそのモデル変換です。
pub struct SceneObject {
mesh : @core.Mesh
transform : @core.Transform3
}
SceneObject::new
SceneObject::new(mesh, transform) はオブジェクトを作ります。同じメッシュを複数のオブジェクトで共有できます。
pub fn SceneObject::new(@core.Mesh, @core.Transform3) -> Self
Light
Light は平行光源で、direction はワールド空間でシーンから光源へ向かう単位ベクトルです。
pub struct Light {
direction : @mutable.Vector[Double]
}
Light::directional
Light::directional(direction) は direction を正規化して光源を作ります。
pub fn Light::directional(@mutable.Vector[Double]) -> Self
零ベクトルを渡すと何も照らさない光源になります。
Light::default
Light::default() は Light::directional((0.6, 0.7, -1.0)) で、既定のカメラと同じ側の右上から照らします。
pub fn Light::default() -> Self
これは通常のコンストラクタであり、Default トレイトの実装ではありません。
Scene
Scene は 1 つの平行光源に照らされたオブジェクトのリストです。
pub struct Scene {
objects : Array[SceneObject]
light : Light
}
Scene::new, Scene::single
Scene::new(light) は空のシーンを、Scene::single(mesh, transform, light) はオブジェクトが 1 つのシーンを作ります。
pub fn Scene::new(Light) -> Self
pub fn Scene::single(@core.Mesh, @core.Transform3, Light) -> Self
Scene::add_object
scene.add_object(object) はその場でシーンにオブジェクトを追加します。
pub fn Scene::add_object(Self, SceneObject) -> Unit
test "scene" {
let scene = @frontend.Scene::new(@frontend.Light::default())
scene.add_object(
@frontend.SceneObject::new(@core.cube_mesh(1.0), @core.Transform3::identity()),
)
scene.add_object(
@frontend.SceneObject::new(
@core.sphere_mesh(0.5, 8, 12),
@core.Transform3::translation(2.0, 0.0, 0.0),
),
)
inspect(scene.objects.length(), content="2")
inspect(@core.vec_length(scene.light.direction), content="1")
}
描画ビューと描画リスト
RenderView
RenderView はシーンの描画に使うカメラと透視投影です。
pub struct RenderView {
camera : @view.Camera3
projection : @view.PerspectiveProjection
}
RenderView::perspective, RenderView::scientific
RenderView::perspective(camera, projection) はカメラと投影を組にします。RenderView::scientific(camera, viewport) は ScientificCamera のカメラを使い、そのレンズとセンサーから投影を導きます。
pub fn RenderView::perspective(@view.Camera3, @view.PerspectiveProjection) -> Self
pub fn RenderView::scientific(@view.ScientificCamera, @view.Viewport) -> Self
DrawTriangle
DrawTriangle は一様な輝度を持つ投影済みの三角形で、輝度は名目上 です。丸めによって 1 を数 ulp 超えることがありますが、バックエンドがクランプします。
pub struct DrawTriangle {
p0 : @view.ProjectedVertex
p1 : @view.ProjectedVertex
p2 : @view.ProjectedVertex
intensity : Double
}
頂点は PerspectiveProjection が出力するとおり、ビューポート単位の座標とカメラ空間の深度を持ちます。画面上の巡回順は正規化されておらず、ラスタライザはどちらの向きも受け付けます。
DrawTriangle::new
DrawTriangle::new(p0, p1, p2, intensity) は三角形を作ります。たとえばシーンなしでバックエンドを動かすときに使います。
pub fn DrawTriangle::new(@view.ProjectedVertex, @view.ProjectedVertex, @view.ProjectedVertex, Double) -> Self
DrawList
DrawList は、すべてのバックエンドが受け取る順序付きの三角形リストです。
pub struct DrawList {
triangles : Array[DrawTriangle]
}
DrawList::new, DrawList::push_triangle
DrawList::new() は空のリストを作り、list.push_triangle(t) はその場で三角形を追加します。
pub fn DrawList::new() -> Self
pub fn DrawList::push_triangle(Self, DrawTriangle) -> Unit
build_draw_list
build_draw_list(scene, view) は幾何パイプラインを実行します。モデル変換、シャドウマップ、ビュー変換、投影、背面カリング、影付きのランバートシェーディング、三角形分割です。
pub fn build_draw_list(Scene, RenderView) -> DrawList
カメラの方を向いた各面について、次の輝度を持つ 2 つの三角形(退化した四角形では 2 つ目の面積は 0)を追加します。
ここで は、128 × 128 のシャドウマップによれば面の中心と 4 頂点のうち光が届く割合です。光源に背を向けた面は になります。三角形はシーンの順にオブジェクトごと、オブジェクト内では面ごとに並び、深度順には並べ替えられません。コストは頂点数と面数に比例し、これにシャドウマップのラスタライズが加わります。
test "draw list" {
let scene = @frontend.Scene::single(
@core.cube_mesh(1.0),
@core.Transform3::rotation(0.4, 0.6, 0.0),
@frontend.Light::default(),
)
let view = @frontend.RenderView::perspective(
@view.Camera3::default(4.5),
@view.PerspectiveProjection::new(@view.Viewport::new(80, 40), 30.0),
)
let list = @frontend.build_draw_list(scene, view)
// three faces of a turned cube face the camera, two triangles each
inspect(list.triangles.length(), content="6")
inspect(list.triangles.all(fn(t) { t.intensity >= 0.0 && t.intensity <= 1.0 }), content="true")
}
輝度バッファ
LUMA_FAR_DEPTH
LUMA_FAR_DEPTH は空のピクセルの深度 です。
pub const LUMA_FAR_DEPTH : Double = 1.0e30
深度が LUMA_FAR_DEPTH * 0.5 以上のピクセルは、バックエンドでは背景として扱われます。
LumaBuffer
LumaBuffer は行優先で並んだスカラー輝度の画像で、ピクセルごとに深度を持ちます。
pub struct LumaBuffer {
width : Int
height : Int
values : Array[Double]
depths : Array[Double]
}
LumaBuffer::new
LumaBuffer::new(width, height) は、すべての値が 0.0、すべての深度が LUMA_FAR_DEPTH のバッファを作ります。
pub fn LumaBuffer::new(Int, Int) -> Self
LumaBuffer::index
buffer.index(x, y) は境界チェックなしで配列インデックス y * width + x を返します。
pub fn LumaBuffer::index(Self, Int, Int) -> Int
LumaBuffer::get, LumaBuffer::depth_at
buffer.get(x, y) はピクセルの値を、buffer.depth_at(x, y) はその深度を返します。バッファ外ではそれぞれ 0.0 と LUMA_FAR_DEPTH を返します。
pub fn LumaBuffer::get(Self, Int, Int) -> Double
pub fn LumaBuffer::depth_at(Self, Int, Int) -> Double
LumaBuffer::set_if_closer
buffer.set_if_closer(x, y, depth, value) は深度テストです。depth + DEPTH_EPSILON が格納済みの深度より小さいときだけピクセルを書き込みます。
pub fn LumaBuffer::set_if_closer(Self, Int, Int, Double, Double) -> Unit
バッファ外への書き込みは無視されます。深度が等しい場合は先に書かれた値が残ります。
LumaBuffer::draw_triangle
buffer.draw_triangle(t) は三角形を 1 つラスタライズします。中心 が三角形の内部または辺上にある各ピクセルは、透視補正された深度で深度テストに通れば t.intensity を受け取ります。
pub fn LumaBuffer::draw_triangle(Self, DrawTriangle) -> Unit
面積が DEPTH_EPSILON 以下の三角形はスキップされます。ループは三角形のバウンディングボックス全体を走査し、それはバッファの範囲に切り詰められません。
draw_list_to_luma
draw_list_to_luma(list, width, height) はバッファを作り、リストの三角形を順に描き込みます。
pub fn draw_list_to_luma(DrawList, Int, Int) -> LumaBuffer
LumaBuffer::add_weighted_sample
buffer.add_weighted_sample(sample, w) はその場で各値に w * sample.values[i] を加え、2 つの深度のうち小さい方を残します。
pub fn LumaBuffer::add_weighted_sample(Self, Self, Double) -> Unit
処理されるのは 2 つの長さの までだけなので、同じサイズのバッファを使ってください。
average_luma
average_luma(a, b) は同じサイズの 2 つのバッファについて、値の平均と深度の最小値を持つ新しいバッファを返します。
pub fn average_luma(LumaBuffer, LumaBuffer) -> LumaBuffer
test "luma buffer" {
let near = @frontend.DrawTriangle::new(
@view.ProjectedVertex::new(0.0, 0.0, 2.0),
@view.ProjectedVertex::new(8.0, 0.0, 2.0),
@view.ProjectedVertex::new(0.0, 8.0, 2.0),
0.8,
)
let far = @frontend.DrawTriangle::new(
@view.ProjectedVertex::new(0.0, 0.0, 5.0),
@view.ProjectedVertex::new(8.0, 0.0, 5.0),
@view.ProjectedVertex::new(0.0, 8.0, 5.0),
0.3,
)
let list = @frontend.DrawList::new()
list.push_triangle(far)
list.push_triangle(near)
let buffer = @frontend.draw_list_to_luma(list, 8, 8)
inspect(buffer.get(1, 1), content="0.8")
inspect(buffer.depth_at(1, 1), content="2")
inspect(buffer.depth_at(7, 7) == @frontend.LUMA_FAR_DEPTH, content="true")
let dark = @frontend.LumaBuffer::new(8, 8)
inspect(@frontend.average_luma(buffer, dark).get(1, 1), content="0.4")
dark.add_weighted_sample(buffer, 0.5)
inspect(dark.get(1, 1), content="0.4")
}
露光
ShutterSpeed
ShutterSpeed は秒単位の露光時間です。
pub struct ShutterSpeed {
seconds : Double
}
ShutterSpeed::seconds, ShutterSpeed::reciprocal
ShutterSpeed::seconds(t) は t 秒、ShutterSpeed::reciprocal(n) は 1/n 秒のシャッターを作ります。正でない引数は 1/60 秒になります。
pub fn ShutterSpeed::seconds(Double) -> Self
pub fn ShutterSpeed::reciprocal(Double) -> Self
ExposureSettings
ExposureSettings は、シャッター、それが属するフレーム間隔、平均するサンプル数からなります。
pub struct ExposureSettings {
shutter : ShutterSpeed
frame_dt : Double
samples : Int
}
ExposureSettings::new
ExposureSettings::new(shutter, frame_dt, samples) は設定を作ります。正でない frame_dt は 1/60 秒になり、シャッターは frame_dt 以下にクランプされ、samples は最低 1 に引き上げられます。
pub fn ExposureSettings::new(ShutterSpeed, Double, Int) -> Self
ExposureSettings::auto
ExposureSettings::auto(shutter, frame_dt) は samples = ceil(shutter / frame_dt) の設定を作ります。この値はシャッターをクランプした後に計算されます。
pub fn ExposureSettings::auto(ShutterSpeed, Double) -> Self
クランプ後のシャッターは決して frame_dt を超えないので、auto は常に 1 サンプルになります。長時間露光が必要な呼び出し側は、TUI デモのように自分で大きなサンプル数を選びます。
test "exposure" {
let shutter = @frontend.ShutterSpeed::reciprocal(30.0)
let settings = @frontend.ExposureSettings::auto(shutter, 1.0 / 60.0)
inspect(settings.shutter.seconds == 1.0 / 60.0, content="true")
inspect(settings.samples, content="1")
let manual = @frontend.ExposureSettings::new(shutter, 1.0 / 24.0, 12)
inspect(manual.samples, content="12")
}
オプティカルフロー
FlowVector
FlowVector は整数のピクセル変位です。
pub struct FlowVector {
dx : Int
dy : Int
}
FlowVector::new
FlowVector::new(dx, dy) は変位を作ります。
pub fn FlowVector::new(Int, Int) -> Self
FlowField
FlowField は行優先で並んだ変位の場で、ピクセルごとに 1 つの変位を持ちます。
pub struct FlowField {
width : Int
height : Int
vectors : Array[FlowVector]
}
FlowField::new, FlowField::index, FlowField::get, FlowField::set
FlowField::new(w, h) は零の場を作ります。index は y * width + x です。get は場の外で (0, 0) を返し、set は場の外への書き込みを無視します。
pub fn FlowField::new(Int, Int) -> Self
pub fn FlowField::index(Self, Int, Int) -> Int
pub fn FlowField::get(Self, Int, Int) -> FlowVector
pub fn FlowField::set(Self, Int, Int, FlowVector) -> Unit
estimate_optical_flow
estimate_optical_flow(previous, current, search_radius, patch_radius) は、current の各ピクセルについて、その近傍が previous のどこから来たかを推定します。
pub fn estimate_optical_flow(LumaBuffer, LumaBuffer, Int, Int) -> FlowField
各ピクセル について、 search_radius を満たす変位 のうち、previous の を中心とする のパッチと current の を中心とするパッチの差の二乗和を最小にするものを返します。ここで = patch_radius です。バッファ外のピクセルは 0.0 として読まれます。同点の場合は走査順(、次に 、それぞれ から増加)で最初の変位が残るので、どの変位も同じくらい合う一様な領域では、結果は ではなく になります。負の半径は 0 として扱われます。コストは です。
align_with_flow
align_with_flow(previous, current, flow) は previous を current のピクセルグリッドへワープします。結果のピクセル は previous の から値と深度を取ります。
pub fn align_with_flow(LumaBuffer, LumaBuffer, FlowField) -> LumaBuffer
accumulate_with_flow
accumulate_with_flow(previous, current, flow) は、ワープした previous と current の平均を、current の深度とともに返します。
pub fn accumulate_with_flow(LumaBuffer, LumaBuffer, FlowField) -> LumaBuffer
test "optical flow" {
let previous = @frontend.LumaBuffer::new(6, 6)
let current = @frontend.LumaBuffer::new(6, 6)
previous.set_if_closer(2, 2, 1.0, 1.0) // a bright pixel at (2, 2)
current.set_if_closer(3, 2, 1.0, 1.0) // has moved one pixel right
let flow = @frontend.estimate_optical_flow(previous, current, 2, 1)
let v = flow.get(3, 2)
debug_inspect((v.dx, v.dy), content="(-1, 0)")
let aligned = @frontend.align_with_flow(previous, current, flow)
inspect(aligned.get(3, 2), content="1")
inspect(@frontend.accumulate_with_flow(previous, current, flow).get(3, 2), content="1")
// far from the moving pixel every displacement fits: the first one wins
let flat = flow.get(0, 5)
debug_inspect((flat.dx, flat.dy), content="(-2, -2)")
}
タイムライン
Timeline
Timeline は一定レートのクリップで、秒単位の長さとフレームレートからなります。
pub struct Timeline {
duration_seconds : Double
fps : Int
}
Timeline::new
Timeline::new(duration, fps) はタイムラインを作ります。正でない長さは 1 秒に、1 未満の fps は 1 になります。
pub fn Timeline::new(Double, Int) -> Self
Timeline::frame_count, Timeline::frame_dt
frame_count は を、frame_dt は を返します。
pub fn Timeline::frame_count(Self) -> Int
pub fn Timeline::frame_dt(Self) -> Double
TimelineSample
TimelineSample はタイムラインの 1 フレームで、インデックス、時刻 、進捗 を持ちます。
pub struct TimelineSample {
frame_index : Int
time_seconds : Double
progress : Double
}
Timeline::sample
timeline.sample(k) はフレーム k のサンプルを返します。負のインデックスは 0 として扱われます。末尾を超えるインデックスも許され、進捗は 1 になります。
pub fn Timeline::sample(Self, Int) -> TimelineSample
ScalarKeyframe
ScalarKeyframe はある時刻の値です。
pub struct ScalarKeyframe {
time_seconds : Double
value : Double
}
ScalarKeyframe::new
ScalarKeyframe::new(time, value) はキーフレームを作ります。
pub fn ScalarKeyframe::new(Double, Double) -> Self
ScalarTrack
ScalarTrack は、時刻順に並んだキーフレームを通る区分線形のアニメーション曲線です。
pub struct ScalarTrack {
keyframes : Array[ScalarKeyframe]
}
ScalarTrack::new
ScalarTrack::new(keyframes) はトラックを作ります。キーフレームは時刻の昇順に並んでいる必要があります。
pub fn ScalarTrack::new(Array[ScalarKeyframe]) -> Self
ScalarTrack::sample
track.sample(t) は時刻 t の値を返します。前後のキーフレームの間では線形補間し、最初のキーフレームより前では最初の値、最後より後では最後の値、空のトラックでは 0.0 を返します。
pub fn ScalarTrack::sample(Self, Double) -> Double
2 つのキーフレームが(DEPTH_EPSILON 以内で)同じ時刻を持つ場合、その時刻では後の値が採用されます。
test "timeline" {
let timeline = @frontend.Timeline::new(1.0, 4)
inspect(timeline.frame_count(), content="4")
let s = timeline.sample(2)
debug_inspect((s.time_seconds, s.progress), content="(0.5, 0.5)")
let track = @frontend.ScalarTrack::new([
@frontend.ScalarKeyframe::new(0.0, 0.0),
@frontend.ScalarKeyframe::new(1.0, 10.0),
])
inspect(track.sample(0.25), content="2.5")
inspect(track.sample(5.0), content="10")
}