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 は一様な輝度を持つ投影済みの三角形で、輝度は名目上 [0,1][0, 1] です。丸めによって 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)を追加します。

I=max⁡(0,n^⋅ℓ) (0.35+0.65 v),v∈{0,0.2,0.4,0.6,0.8,1},I = \max(0, \hat n \cdot \ell)\,\big(0.35 + 0.65\,v\big),\qquad v \in \{0, 0.2, 0.4, 0.6, 0.8, 1\},

ここで vv は、128 × 128 のシャドウマップによれば面の中心と 4 頂点のうち光が届く割合です。光源に背を向けた面は I=0I = 0 になります。三角形はシーンの順にオブジェクトごと、オブジェクト内では面ごとに並び、深度順には並べ替えられません。コストは頂点数と面数に比例し、これにシャドウマップのラスタライズが加わります。

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 は空のピクセルの深度 103010^{30} です。

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 つラスタライズします。中心 (x+12,y+12)(x + \tfrac12, y + \tfrac12) が三角形の内部または辺上にある各ピクセルは、透視補正された深度で深度テストに通れば 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 つの長さの min⁡\min までだけなので、同じサイズのバッファを使ってください。

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

各ピクセル (x,y)(x, y) について、∣dx∣,∣dy∣≤|d_x|, |d_y| \le search_radius を満たす変位 (dx,dy)(d_x, d_y) のうち、previous の (x+dx,y+dy)(x + d_x, y + d_y) を中心とする (2P+1)2(2P + 1)^2 のパッチと current の (x,y)(x, y) を中心とするパッチの差の二乗和を最小にするものを返します。ここで PP = patch_radius です。バッファ外のピクセルは 0.0 として読まれます。同点の場合は走査順(dyd_y、次に dxd_x、それぞれ −R-R から増加)で最初の変位が残るので、どの変位も同じくらい合う一様な領域では、結果は (0,0)(0, 0) ではなく (−R,−R)(-R, -R) になります。負の半径は 0 として扱われます。コストは O(WH(2R+1)2(2P+1)2)O\big(W H (2R + 1)^2 (2P + 1)^2\big) です。

align_with_flow

align_with_flow(previous, current, flow) は previous を current のピクセルグリッドへワープします。結果のピクセル (x,y)(x, y) は previous の (x+dx,y+dy)(x + d_x, y + d_y) から値と深度を取ります。

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 は max⁡(1,⌈D⋅fps⌉)\max(1, \lceil D \cdot \mathit{fps} \rceil) を、frame_dt は 1/fps1/\mathit{fps} を返します。

pub fn Timeline::frame_count(Self) -> Int
pub fn Timeline::frame_dt(Self) -> Double

TimelineSample

TimelineSample はタイムラインの 1 フレームで、インデックス、時刻 t=k/fpst = k/\mathit{fps}、進捗 min⁡(1,t/D)\min(1, t/D) を持ちます。

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")
}