view API

パッケージ Luna-Flow/geometry3d/view は、点をワールド空間から画面へ運びます。ルックアット(look-at)カメラ、ビューポートへの透視投影と正射影、すべてのラスタライザが使う透視補正された深度補間、そして焦点距離から投影スケールを導く物理カメラモデル(センサー、レンズ、ワールド単位)を含みます。ラスタライズは行わず、ターミナルや DOM については何も知りません。

import {
  "Luna-Flow/geometry3d/core",
  "Luna-Flow/geometry3d/view",
  "Luna-Flow/linear-algebra/mutable" @la,
}

座標は一貫して 1 つの規約に従います。カメラ空間では xx が右、yy が上、zz が前を向くので、見える点は z>0z > 0 です。画面上では、ビューポートのピクセル(またはターミナルのセル)単位で xx は右へ、yy は下へ増えます。このページの式はすべて view の設計で導出しています。

カメラ

Camera3

Camera3 はルックアットカメラで、視点の位置、注視点、おおよその上方向をすべてワールド空間で持ちます。

pub struct Camera3 {
  eye : @mutable.Vector[Double]
  target : @mutable.Vector[Double]
  up : @mutable.Vector[Double]
}

up は視線方向と直交している必要はなく、カメラはそこから正規直交の座標系を導きます。ただし target - eye と平行であってはいけません。

Camera3::look_at

Camera3::look_at(eye, target, up) は 3 つのベクトルから検証なしでカメラを作ります。

pub fn Camera3::look_at(@mutable.Vector[Double], @mutable.Vector[Double], @mutable.Vector[Double]) -> Self

Camera3::default

Camera3::default(distance) は視点を (0,0,−distance)(0, 0, -\mathit{distance}) に置き、上方向 =+y= +y で原点を向かせます。

pub fn Camera3::default(Double) -> Self

これは通常のコンストラクタであり、Default トレイトの実装ではありません。

Camera3::forward, Camera3::right, Camera3::true_up

これらのメソッドはカメラの正規直交座標系を返します。

pub fn Camera3::forward(Self) -> @mutable.Vector[Double]
pub fn Camera3::right(Self) -> @mutable.Vector[Double]
pub fn Camera3::true_up(Self) -> @mutable.Vector[Double]

それぞれ次のとおりです。

f=normalize⁡(target−eye),r=normalize⁡(up×f),u=normalize⁡(f×r).f = \operatorname{normalize}(\mathit{target} - \mathit{eye}),\qquad r = \operatorname{normalize}(\mathit{up} \times f),\qquad u = \operatorname{normalize}(f \times r).

rr、uu、ff は互いに直交する単位ベクトルで、r×u=fr \times u = f を満たします。up が ff と平行なら rr と uu は零ベクトルになり、カメラは使えません。

Camera3::view_transform

camera.view_transform() はワールドからカメラへの変換を返します。これは視点を原点に、座標系 (r,u,f)(r, u, f) を軸 (x,y,z)(x, y, z) に移す剛体運動です。

pub fn Camera3::view_transform(Self) -> @core.Transform3

その行列の行は (rT,−r⋅e)(r^\mathsf{T}, -r \cdot e)、(uT,−u⋅e)(u^\mathsf{T}, -u \cdot e)、(fT,−f⋅e)(f^\mathsf{T}, -f \cdot e)、(0,0,0,1)(0, 0, 0, 1) で、ee は視点です。

Camera3::world_to_camera_point, Camera3::world_to_camera_direction

これらのメソッドはビュー変換を点または方向に適用します。

pub fn Camera3::world_to_camera_point(Self, @mutable.Vector[Double]) -> @mutable.Vector[Double]
pub fn Camera3::world_to_camera_direction(Self, @mutable.Vector[Double]) -> @mutable.Vector[Double]

点は (r⋅(p−e), u⋅(p−e), f⋅(p−e))(r \cdot (p - e),\ u \cdot (p - e),\ f \cdot (p - e)) に、方向は (r⋅d, u⋅d, f⋅d)(r \cdot d,\ u \cdot d,\ f \cdot d) になります。呼び出すたびにビュー行列を作り直すので、多数の点を写すときは view_transform の結果を保持してください。

test "camera" {
  let camera = @view.Camera3::default(4.5)
  inspect(camera.forward()[2], content="1")
  inspect(camera.right()[0], content="1")
  inspect(camera.true_up()[1], content="1")
  let p = camera.world_to_camera_point(@core.vec3(1.0, 2.0, 0.0))
  inspect(p[0], content="1")
  inspect(p[1], content="2")
  inspect(p[2], content="4.5")
  let d = camera.world_to_camera_direction(@core.vec3(0.0, 0.0, 1.0))
  inspect(d[2], content="1")
}

ビューポートと投影済みの頂点

Viewport

Viewport はピクセルまたはターミナルのセル単位の出力サイズです。

pub struct Viewport {
  width : Int
  height : Int
}

Viewport::new

Viewport::new(width, height) は検証なしでビューポートを作ります。

pub fn Viewport::new(Int, Int) -> Self

ProjectedVertex

ProjectedVertex は投影後の頂点で、ビューポート単位の画面座標 x、y と、元の点のカメラ空間での zz である depth を持ちます。

pub struct ProjectedVertex {
  x : Double
  y : Double
  depth : Double
}

depth は視線方向に沿った距離であり、正規化デバイス深度ではありません。小さいほど近くにあります。

ProjectedVertex::new

ProjectedVertex::new(x, y, depth) は投影済みの頂点を作ります。たとえばラスタライザに直接渡すときに使います。

pub fn ProjectedVertex::new(Double, Double, Double) -> Self

投影

PerspectiveProjection

PerspectiveProjection はビューポートへのピンホール投影で、スケール ss は x/zx/z の 1 単位あたりのピクセル数です。

pub struct PerspectiveProjection {
  viewport : Viewport
  scale : Double
}

PerspectiveProjection::new

PerspectiveProjection::new(viewport, scale) は投影を作ります。レンズから scale を導くには ScientificCamera::to_perspective_projection を使います。

pub fn PerspectiveProjection::new(Viewport, Double) -> Self

PerspectiveProjection::project_point

projection.project_point(p) はカメラ空間の点を画面に写します。

pub fn PerspectiveProjection::project_point(Self, @mutable.Vector[Double]) -> ProjectedVertex
xs=W2+s xz,ys=H2−s yz,depth=z,x_s = \frac{W}{2} + s\,\frac{x}{z},\qquad y_s = \frac{H}{2} - s\,\frac{y}{z},\qquad \mathit{depth} = z ,

ここで W×HW \times H はビューポートです。点はカメラの前方(z>0z > 0)になければなりません。ニア平面もクリッピングもないので、z=0z = 0 では無限大が、z<0z < 0 では鏡像が生じます。

OrthographicProjection

OrthographicProjection はビューポートへの平行投影で、スケール ss はワールド単位あたりのピクセル数です。

pub struct OrthographicProjection {
  viewport : Viewport
  scale : Double
}

OrthographicProjection::new

OrthographicProjection::new(viewport, scale) は投影を作ります。

pub fn OrthographicProjection::new(Viewport, Double) -> Self

OrthographicProjection::project_point

projection.project_point(p) はカメラ空間の点を xs=W/2+sxx_s = W/2 + s x、ys=H/2−syy_s = H/2 - s y、depth=z\mathit{depth} = z で写します。

pub fn OrthographicProjection::project_point(Self, @mutable.Vector[Double]) -> ProjectedVertex

project_perspective_vertices, project_orthographic_vertices

これらの関数は配列のすべての点を指定した投影で投影します。

pub fn project_perspective_vertices(Array[@mutable.Vector[Double]], PerspectiveProjection) -> Array[ProjectedVertex]
pub fn project_orthographic_vertices(Array[@mutable.Vector[Double]], OrthographicProjection) -> Array[ProjectedVertex]

出力は入力と長さも順序も同じなので、メッシュの面インデックスはそのまま有効です。

test "projections" {
  let viewport = @view.Viewport::new(80, 40)
  let persp = @view.PerspectiveProjection::new(viewport, 20.0)
  let near = persp.project_point(@core.vec3(1.0, 1.0, 2.0))
  let far = persp.project_point(@core.vec3(1.0, 1.0, 4.0))
  inspect(near.x, content="50")
  inspect(near.y, content="10")
  inspect(far.x, content="45")
  inspect(far.depth, content="4")
  let ortho = @view.OrthographicProjection::new(viewport, 20.0)
  inspect(ortho.project_point(@core.vec3(1.0, 1.0, 4.0)).x, content="60")
  let all = @view.project_perspective_vertices(
    [@core.vec3(0.0, 0.0, 1.0), @core.vec3(0.0, 0.0, 2.0)],
    persp,
  )
  inspect(all.length(), content="2")
}

深度の補間

interpolate_perspective_depth

interpolate_perspective_depth(p0, p1, p2, b0, b1, b2) は、投影された三角形 p0p1p2p_0 p_1 p_2 の中で重心座標 (b0,b1,b2)(b_0, b_1, b_2) を持つ画面上の点の深度を返します。

pub fn interpolate_perspective_depth(ProjectedVertex, ProjectedVertex, ProjectedVertex, Double, Double, Double) -> Double

結果は次のとおりです。

z=(b0z0+b1z1+b2z2)−1,z = \left( \frac{b_0}{z_0} + \frac{b_1}{z_1} + \frac{b_2}{z_2} \right)^{-1},

三角形が PerspectiveProjection で投影されたものであれば、これは 3D 三角形上の対応する点のカメラ空間での正確な深度です。いずれかの zi≤z_i \le DEPTH_EPSILON の場合、または和が 0 から DEPTH_EPSILON 以内の場合は、線形の b0z0+b1z1+b2z2b_0 z_0 + b_1 z_1 + b_2 z_2 に切り替えます。重心座標の和は 1 である必要があります。

test "perspective-correct depth" {
  let a = @view.ProjectedVertex::new(0.0, 0.0, 1.0)
  let b = @view.ProjectedVertex::new(10.0, 0.0, 3.0)
  let c = @view.ProjectedVertex::new(0.0, 10.0, 1.0)
  // halfway along the edge from a to b on the screen
  let z = @view.interpolate_perspective_depth(a, b, c, 0.5, 0.5, 0.0)
  inspect(z, content="1.5")
  // the linear average would have been 2.0
}

物理カメラ

SensorSpec

SensorSpec はミリメートル単位のカメラセンサーのサイズです。

pub struct SensorSpec {
  width_mm : Double
  height_mm : Double
}

SensorSpec::custom

SensorSpec::custom(width_mm, height_mm) はセンサーを作ります。正でない寸法は 1.0 に置き換えられます。

pub fn SensorSpec::custom(Double, Double) -> Self

SensorSpec::full_frame, SensorSpec::apsc, SensorSpec::medium_format

これらのプリセットは 36 × 24 mm、23.5 × 15.6 mm、44 × 33 mm です。

pub fn SensorSpec::full_frame() -> Self
pub fn SensorSpec::apsc() -> Self
pub fn SensorSpec::medium_format() -> Self

SensorSpec::diagonal_mm

sensor.diagonal_mm() は w2+h2\sqrt{w^2 + h^2} を返します。

pub fn SensorSpec::diagonal_mm(Self) -> Double

LensSpec

LensSpec はミリメートル単位の焦点距離で表されるレンズです。

pub struct LensSpec {
  focal_length_mm : Double
}

LensSpec::new, LensSpec::normal_full_frame

LensSpec::new(f) はレンズを作り、正でない焦点距離を 1.0 に置き換えます。LensSpec::normal_full_frame() は 50 mm レンズです。

pub fn LensSpec::new(Double) -> Self
pub fn LensSpec::normal_full_frame() -> Self

LensSpec::horizontal_fov, LensSpec::vertical_fov, LensSpec::diagonal_fov

これらのメソッドは、センサーの幅、高さ、または対角線にわたる画角をラジアンで返します。

pub fn LensSpec::horizontal_fov(Self, SensorSpec) -> Double
pub fn LensSpec::vertical_fov(Self, SensorSpec) -> Double
pub fn LensSpec::diagonal_fov(Self, SensorSpec) -> Double

センサーの寸法 dd と焦点距離 ff について、いずれも 2arctan⁡ ⁣(d/(2f))2 \arctan\!\big(d / (2 f)\big) です。

WorldUnit

WorldUnit はワールド空間の 1 単位が何メートルを表すかを示します。

pub struct WorldUnit {
  meters_per_unit : Double
}

WorldUnit::new, WorldUnit::unitless

WorldUnit::new(m) は単位を作り、正でない値を 1.0 に置き換えます。WorldUnit::unitless() は 1 単位あたり 1 メートルです。

pub fn WorldUnit::new(Double) -> Self
pub fn WorldUnit::unitless() -> Self

WorldUnit::millimeters_per_unit

unit.millimeters_per_unit() は meters_per_unit * 1000 を返します。

pub fn WorldUnit::millimeters_per_unit(Self) -> Double

ScientificCamera

ScientificCamera は Camera3 をセンサー、レンズ、ワールド単位とまとめたものです。

pub struct ScientificCamera {
  camera : Camera3
  sensor : SensorSpec
  lens : LensSpec
  world_unit : WorldUnit
}

ScientificCamera::new

ScientificCamera::new(camera, sensor, lens, world_unit) は各部品からカメラを作ります。

pub fn ScientificCamera::new(Camera3, SensorSpec, LensSpec, WorldUnit) -> Self

ScientificCamera::auto

ScientificCamera::auto(viewport) は、フルサイズセンサー、50 mm レンズ、単位なしのワールド単位を備えた Camera3::default(4.5) を返します。

pub fn ScientificCamera::auto(Viewport) -> Self

ビューポートの引数は現在無視されます。

ScientificCamera::with_camera, ScientificCamera::with_lens

これらのメソッドはカメラまたはレンズを置き換えたコピーを返します。ドリーやズームをアニメーションさせる手段です。

pub fn ScientificCamera::with_camera(Self, Camera3) -> Self
pub fn ScientificCamera::with_lens(Self, LensSpec) -> Self

ScientificCamera::projection_scale

camera.projection_scale(viewport) は透視スケール s=Hf/hs = H f / h を返します。HH はビューポートの高さ、ff は焦点距離、hh はセンサーの高さです。

pub fn ScientificCamera::projection_scale(Self, Viewport) -> Double

このスケールでは、垂直方向の画角がちょうどビューポートの高さに収まります。

ScientificCamera::to_perspective_projection

camera.to_perspective_projection(viewport) は PerspectiveProjection::new(viewport, camera.projection_scale(viewport)) を返します。

pub fn ScientificCamera::to_perspective_projection(Self, Viewport) -> PerspectiveProjection

ScientificCamera::focal_length_world_units, ScientificCamera::sensor_height_world_units

これらのメソッドは焦点距離とセンサーの高さをミリメートルからワールド単位に換算します。

pub fn ScientificCamera::focal_length_world_units(Self) -> Double
pub fn ScientificCamera::sensor_height_world_units(Self) -> Double

これらは参考情報です。投影スケールは比 f/hf / h だけで決まり、そこでは単位が打ち消し合います。

test "scientific camera" {
  let sensor = @view.SensorSpec::full_frame()
  let lens = @view.LensSpec::normal_full_frame()
  let degrees = lens.vertical_fov(sensor) * 180.0 / @math.PI
  inspect((degrees * 10.0).round() / 10.0, content="27")
  let camera = @view.ScientificCamera::auto(@view.Viewport::new(640, 480))
  inspect(camera.projection_scale(@view.Viewport::new(640, 480)), content="1000")
  let mm = @view.ScientificCamera::new(
    @view.Camera3::default(4500.0),
    sensor,
    lens,
    @view.WorldUnit::new(0.001),
  )
  inspect(mm.focal_length_world_units(), content="50")
  let tele = camera.with_lens(@view.LensSpec::new(100.0))
  inspect(tele.projection_scale(@view.Viewport::new(640, 480)), content="2000")
}