view API

包 Luna-Flow/geometry3d/view 把点从世界空间变换到屏幕上。它包含观察(look-at)相机、到视口的透视投影和正交投影、所有光栅化器共用的透视校正深度插值,以及根据焦距推导投影比例的物理相机模型(传感器、镜头、世界单位)。它不做光栅化,对终端和 DOM 一无所知。

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

坐标始终遵循同一约定。在相机空间中 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) 由三个向量构造相机,不做校验。

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 trait 的实现。

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 对应的像素数计。

pub struct PerspectiveProjection {
  viewport : Viewport
  scale : Double
}

PerspectiveProjection::new

PerspectiveProjection::new(viewport, scale) 构造该投影。使用 ScientificCamera::to_perspective_projection 可以根据镜头推导 scale。

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,或者和与零之差在 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 表示一个世界空间单位代表多少米。

pub struct WorldUnit {
  meters_per_unit : Double
}

WorldUnit::new, WorldUnit::unitless

WorldUnit::new(m) 构造该单位,非正值会被替换为 1.0;WorldUnit::unitless() 表示每单位一米。

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) 返回 Camera3::default(4.5),搭配全画幅传感器、50 mm 镜头和无量纲的世界单位。

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