core API

包 Luna-Flow/geometry3d/core 承载管线中的 3D 数据和仿射数学:向量、四边形网格、网格生成器、面法向量、背面可见性、朗伯亮度以及 4×4 齐次变换。它对相机、屏幕、终端和浏览器一无所知。向量和矩阵是 Luna-Flow/linear-algebra 中的可变稠密类型,这里以 @la 导入。

按如下方式导入:

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

core 设计推导了下文用到的公式,core 教程通过示例逐一讲解。

常量

DEPTH_EPSILON

DEPTH_EPSILON 是整个管线共用的容差。

pub const DEPTH_EPSILON : Double = 1.0e-9

它用作:normalize_vec 返回零向量的长度阈值,Transform3::apply_point 跳过齐次除法的 w 阈值,光栅化的最小三角形面积,以及 view、frontend 和各后端包中每次深度测试的余量。

向量

所有向量辅助函数都接受并返回 @la.Vector[Double]。它们只读取所需的分量(3D 辅助函数读取下标 0..2),因此传给 3D 辅助函数的四分量向量会被视为其前三个分量。

vec3

vec3 构造三分量向量 (x,y,z)(x, y, z)。

pub fn vec3(Double, Double, Double) -> @mutable.Vector[Double]

vec4

vec4 构造四分量向量 (x,y,z,w)(x, y, z, w),即 Transform3 内部使用的齐次形式。

pub fn vec4(Double, Double, Double, Double) -> @mutable.Vector[Double]

sub_vec

sub_vec(a, b) 返回三维差 a−ba - b。

pub fn sub_vec(@mutable.Vector[Double], @mutable.Vector[Double]) -> @mutable.Vector[Double]

cross_vec

cross_vec(a, b) 返回叉积 a×b=(aybz−azby, azbx−axbz, axby−aybx)a \times b = (a_y b_z - a_z b_y,\ a_z b_x - a_x b_z,\ a_x b_y - a_y b_x)。

pub fn cross_vec(@mutable.Vector[Double], @mutable.Vector[Double]) -> @mutable.Vector[Double]

vec_length

vec_length(v) 返回 v 所有分量上的欧几里得范数 ∥v∥=v⋅v\lVert v \rVert = \sqrt{v \cdot v}。

pub fn vec_length(@mutable.Vector[Double]) -> Double

不要把它与返回分量个数的 @la.Vector::length 混淆。

normalize_vec

normalize_vec(v) 返回 v/∥v∥v / \lVert v \rVert;当 ∥v∥≤\lVert v \rVert \le DEPTH_EPSILON 时返回零向量 (0,0,0)(0, 0, 0)。

pub fn normalize_vec(@mutable.Vector[Double]) -> @mutable.Vector[Double]

零结果使退化面无害:它们的法向量为零,因此永远不可见、也永远不会被照亮。

test "vector helpers" {
  let x = @core.vec3(1.0, 0.0, 0.0)
  let y = @core.vec3(0.0, 1.0, 0.0)
  let z = @core.cross_vec(x, y)
  inspect(z[2], content="1")
  inspect(@core.vec_length(@core.vec3(3.0, 4.0, 0.0)), content="5")
  let n = @core.normalize_vec(@core.vec3(0.0, 0.0, 1.0e-12))
  inspect(@core.vec_length(n), content="0")
  inspect(@core.sub_vec(z, x)[0], content="-1")
}

min3, max3

min3 和 max3 返回三个数中的最小值和最大值。光栅化器用它们计算三角形的包围盒。

pub fn min3(Double, Double, Double) -> Double
pub fn max3(Double, Double, Double) -> Double
test "min3 and max3" {
  inspect(@core.min3(2.0, -1.0, 0.5), content="-1")
  inspect(@core.max3(2.0, -1.0, 0.5), content="2")
}

网格

QuadFace

QuadFace 是网格的一个面,由指向顶点数组的四个下标给出。

pub struct QuadFace {
  a : Int
  b : Int
  c : Int
  d : Int
}

顶点的排列使 (b−a)×(c−a)(b - a) \times (c - a) 指向封闭网格的外部。三角形存储为退化四边形,其最后一个下标重复第一个(d=ad = a)。

TriangleFace

TriangleFace 是由三个顶点下标给出的三角形;triangulate_quad 为光栅化器生成它。

pub struct TriangleFace {
  a : Int
  b : Int
  c : Int
}

Mesh

Mesh 是一个索引面集:顶点位置加上引用它们的四边形面。

pub struct Mesh {
  vertices : Array[@mutable.Vector[Double]]
  faces : Array[QuadFace]
}

Mesh、QuadFace 和 TriangleFace 的字段在包外是只读的,因此你需要从下面的生成器获得网格,并用 Transform3::apply_mesh 改变它们。

cube_mesh, sphere_mesh, cylinder_mesh, cone_mesh, triangular_pyramid_mesh, torus_mesh

这些生成器构造封闭的基本网格。

每个生成器都返回一个以原点为中心、向外环绕的网格,并把分辨率参数提升到至少 3。

生成器形状顶点数面数
cube_mesh(s)立方体 [−s,s]3[-s, s]^386
sphere_mesh(r, rings, segments)半径为 rr 的 UV 球,两极位于 yy 轴上2+(rings−1) segments2 + (\mathit{rings} - 1)\,\mathit{segments}rings⋅segments\mathit{rings} \cdot \mathit{segments}
cylinder_mesh(r, h, n)沿 yy 轴、带端盖的圆柱,y∈[−h/2,h/2]y \in [-h/2, h/2]2n+22n + 23n3n
cone_mesh(r, h, n)带底面的圆锥,顶点在 y=h/2y = h/2,底面在 y=−h/2y = -h/2n+2n + 22n2n
triangular_pyramid_mesh(r, h)四面体,其底面三角形内接于半径为 rr 的圆44
torus_mesh(R, r, M, m)绕 yy 轴的圆环MmM mMmM m
pub fn cube_mesh(Double) -> Mesh
pub fn sphere_mesh(Double, Int, Int) -> Mesh
pub fn cylinder_mesh(Double, Double, Int) -> Mesh
pub fn cone_mesh(Double, Double, Int) -> Mesh
pub fn triangular_pyramid_mesh(Double, Double) -> Mesh
pub fn torus_mesh(Double, Double, Int, Int) -> Mesh

端盖、圆锥侧面和棱锥的面都是退化四边形(d=ad = a)。torus_mesh 还会把非正的主半径替换为 1.0、把非正的次半径替换为 0.25;其他生成器按原样使用尺寸参数。

test "mesh generators" {
  let cube = @core.cube_mesh(1.0)
  inspect(cube.vertices.length(), content="8")
  inspect(cube.faces.length(), content="6")
  let sphere = @core.sphere_mesh(2.4, 18, 36)
  inspect(sphere.vertices.length(), content="614")
  inspect(sphere.faces.length(), content="648")
  let torus = @core.torus_mesh(2.1, 0.72, 28, 16)
  inspect(torus.faces.length(), content="448")
  inspect(@core.cone_mesh(1.0, 2.0, 2).faces.length(), content="6")
}

face_vertices

face_vertices(mesh, face) 按 a、b、c、d 的顺序返回 face 的四个顶点。

pub fn face_vertices(Mesh, QuadFace) -> Array[@mutable.Vector[Double]]

这些向量与 mesh.vertices 共享,而不是副本。

triangulate_quad

triangulate_quad(face) 把四边形拆成扇形 (a,b,c)(a, b, c)、(a,c,d)(a, c, d)。

pub fn triangulate_quad(QuadFace) -> Array[TriangleFace]

两个三角形都保持四边形的环绕方向。对于退化四边形,第二个三角形 (a,c,a)(a, c, a) 面积为零,光栅化器会跳过它。

test "face vertices and triangulation" {
  let cube = @core.cube_mesh(1.0)
  let face = cube.faces[0]
  inspect(@core.face_vertices(cube, face).length(), content="4")
  let triangles = @core.triangulate_quad(face)
  inspect(triangles.length(), content="2")
  inspect(triangles[1].a == face.a && triangles[1].c == face.d, content="true")
}

面数

面辅助函数把顶点数组与面分开传入,因此你可以传入已变换到另一空间(例如相机空间)的顶点,同时复用原网格的面列表。

face_center

face_center(vertices, face) 返回面的四个顶点的平均值 14(va+vb+vc+vd)\tfrac14(v_a + v_b + v_c + v_d)。

pub fn face_center(Array[@mutable.Vector[Double]], QuadFace) -> @mutable.Vector[Double]

对于退化四边形,重复的顶点会被计算两次。

face_normal

face_normal(vertices, face) 返回由前三个顶点计算的单位法向量 n^=normalize⁡((vb−va)×(vc−va))\hat n = \operatorname{normalize}((v_b - v_a) \times (v_c - v_a))。

pub fn face_normal(Array[@mutable.Vector[Double]], QuadFace) -> @mutable.Vector[Double]

当三个顶点共线时,它返回零向量。

face_is_visible

face_is_visible(vertices, face, eye) 是背面测试:当 n^⋅(e−c)>0\hat n \cdot (e - c) > 0 时返回 true,其中 cc 是面的中心。

pub fn face_is_visible(Array[@mutable.Vector[Double]], QuadFace, @mutable.Vector[Double]) -> Bool

face_intensity

face_intensity(vertices, face, light) 返回朗伯项 max⁡(0,n^⋅ℓ)\max(0, \hat n \cdot \ell)。

pub fn face_intensity(Array[@mutable.Vector[Double]], QuadFace, @mutable.Vector[Double]) -> Double

light 应为从表面指向光源的单位向量;此时结果就是入射角的余弦,位于 [0,1][0, 1] 中。

test "face helpers" {
  let cube = @core.cube_mesh(1.0)
  let front = cube.faces[0] // the face at z = -1
  let n = @core.face_normal(cube.vertices, front)
  inspect(n[2], content="-1")
  let c = @core.face_center(cube.vertices, front)
  inspect(c[2], content="-1")
  let eye = @core.vec3(0.0, 0.0, -4.5)
  inspect(@core.face_is_visible(cube.vertices, front, eye), content="true")
  inspect(@core.face_is_visible(cube.vertices, cube.faces[1], eye), content="false")
  let light = @core.vec3(0.0, 0.0, -1.0)
  inspect(@core.face_intensity(cube.vertices, front, light), content="1")
}

矩阵

矩阵构造函数返回 4×4 的 @la.Matrix[Double],作用于列向量 (x,y,z,w)T(x, y, z, w)^\mathsf{T}。角度以弧度为单位。

identity_matrix4

identity_matrix4() 返回 4×4 单位矩阵 I4I_4。

pub fn identity_matrix4() -> @mutable.Matrix[Double]

translation_matrix

translation_matrix(x, y, z) 返回 (I3t01)\begin{pmatrix} I_3 & t \\ 0 & 1 \end{pmatrix},其中 t=(x,y,z)Tt = (x, y, z)^\mathsf{T}。

pub fn translation_matrix(Double, Double, Double) -> @mutable.Matrix[Double]

scale_matrix

scale_matrix(x, y, z) 返回 diag⁡(x,y,z,1)\operatorname{diag}(x, y, z, 1)。

pub fn scale_matrix(Double, Double, Double) -> @mutable.Matrix[Double]

rotation_x, rotation_y, rotation_z

rotation_x(θ)、rotation_y(θ) 和 rotation_z(θ) 返回绕各坐标轴旋转 θ\theta 的旋转矩阵。

pub fn rotation_x(Double) -> @mutable.Matrix[Double]
pub fn rotation_y(Double) -> @mutable.Matrix[Double]
pub fn rotation_z(Double) -> @mutable.Matrix[Double]

它们的 3×3 块为

Rx(θ)=(1000cos⁡θ−sin⁡θ0sin⁡θcos⁡θ),Ry(θ)=(cos⁡θ0sin⁡θ010−sin⁡θ0cos⁡θ),Rz(θ)=(cos⁡θ−sin⁡θ0sin⁡θcos⁡θ0001).R_x(\theta) = \begin{pmatrix} 1 & 0 & 0 \\ 0 & \cos\theta & -\sin\theta \\ 0 & \sin\theta & \cos\theta \end{pmatrix},\quad R_y(\theta) = \begin{pmatrix} \cos\theta & 0 & \sin\theta \\ 0 & 1 & 0 \\ -\sin\theta & 0 & \cos\theta \end{pmatrix},\quad R_z(\theta) = \begin{pmatrix} \cos\theta & -\sin\theta & 0 \\ \sin\theta & \cos\theta & 0 \\ 0 & 0 & 1 \end{pmatrix}.

正角度分别把 yy 转向 zz、把 zz 转向 xx、把 xx 转向 yy。

rotation_matrix

rotation_matrix(α, β, γ) 返回欧拉角旋转 Rz(γ) Ry(β) Rx(α)R_z(\gamma)\,R_y(\beta)\,R_x(\alpha)。

pub fn rotation_matrix(Double, Double, Double) -> @mutable.Matrix[Double]

作用于列向量时,先绕 xx 旋转,再绕 yy,最后绕 zz,都是绕固定的世界坐标轴。core 设计给出了完整矩阵及其万向节锁行为。

test "matrix builders" {
  let r = @core.rotation_z(@math.PI / 2.0)
  let p = r.unchecked_mul_vec(@core.vec4(1.0, 0.0, 0.0, 1.0))
  inspect(p[1], content="1")
  let t = @core.translation_matrix(1.0, 2.0, 3.0)
  let q = t.unchecked_mul_vec(@core.vec4(0.0, 0.0, 0.0, 1.0))
  inspect(q[2], content="3")
  let m = @core.rotation_matrix(0.0, 0.0, 0.0)
  inspect(m.get(0, 0) == 1.0 && m.get(0, 1) == 0.0, content="true")
}

变换

Transform3

Transform3 包装一个 4×4 齐次矩阵,并把它作用于点、方向和网格。

pub struct Transform3 {
  matrix : @mutable.Matrix[Double]
}

Transform3::identity, Transform3::translation, Transform3::scale, Transform3::rotation

这些构造函数分别包装 identity_matrix4、translation_matrix、scale_matrix 和 rotation_matrix。

pub fn Transform3::identity() -> Self
pub fn Transform3::translation(Double, Double, Double) -> Self
pub fn Transform3::scale(Double, Double, Double) -> Self
pub fn Transform3::rotation(Double, Double, Double) -> Self

Transform3::rotation_euler

Transform3::rotation_euler(α, β, γ) 与 Transform3::rotation(α, β, γ) 相同;这个名字点明了角度约定。

pub fn Transform3::rotation_euler(Double, Double, Double) -> Self

Transform3::from_matrix

Transform3::from_matrix(m) 包装任意 4×4 矩阵,包括最后一行不是 (0,0,0,1)(0, 0, 0, 1) 的射影矩阵。

pub fn Transform3::from_matrix(@mutable.Matrix[Double]) -> Self

矩阵按引用保存而不复制,并且必须是 4×4:对其他形状,apply 系列方法会中止。

Transform3::compose

t.compose(next) 返回先应用 t、再应用 next 的变换;其矩阵为 next.matrix * t.matrix。

pub fn Transform3::compose(Self, Self) -> Self

组合满足结合律但不满足交换律。请从左到右阅读一条链:scale.compose(rotation).compose(translation) 先缩放,再旋转,最后平移。

Transform3::apply_point

t.apply_point(p) 通过 M(px,py,pz,1)T=(x′,y′,z′,w′)TM (p_x, p_y, p_z, 1)^\mathsf{T} = (x', y', z', w')^\mathsf{T} 映射点 pp,并返回 (x′/w′,y′/w′,z′/w′)(x'/w', y'/w', z'/w')。

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

对于仿射变换有 w′=1w' = 1。当 ∣w′∣≤|w'| \le DEPTH_EPSILON 时跳过除法,直接返回 (x′,y′,z′)(x', y', z')。

Transform3::apply_direction

t.apply_direction(d) 以 w=0w = 0 映射方向 dd,因此平移不影响它,并在不做除法的情况下返回前三个分量。

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

结果不会被重新归一化。管线中的法向量不是这样变换的:它们由变换后的顶点重新计算。

Transform3::apply_vertex

t.apply_vertex(v) 与 t.apply_point(v) 相同。

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

Transform3::apply_mesh

t.apply_mesh(mesh) 返回一个新网格,其每个顶点都经 apply_point 映射,面数组保持不变。

pub fn Transform3::apply_mesh(Self, Mesh) -> Mesh

面数组与输入网格共享;由于面是不可变的,这样做是安全的。

test "transforms" {
  let shift = @core.Transform3::translation(1.0, 0.0, 0.0)
  let turn = @core.Transform3::rotation(0.0, 0.0, @math.PI / 2.0)
  let p = @core.vec3(1.0, 0.0, 0.0)
  // rotate first, then translate: (1,0,0) -> (0,1,0) -> (1,1,0)
  let a = turn.compose(shift).apply_point(p)
  inspect((a[0] - 1.0).abs() < 1.0e-12 && (a[1] - 1.0).abs() < 1.0e-12, content="true")
  // translate first, then rotate: (1,0,0) -> (2,0,0) -> (0,2,0)
  let b = shift.compose(turn).apply_point(p)
  inspect((b[1] - 2.0).abs() < 1.0e-12, content="true")
  // directions ignore the translation
  let d = shift.apply_direction(p)
  inspect(d[0], content="1")
  let cube = @core.Transform3::scale(2.0, 2.0, 2.0).apply_mesh(@core.cube_mesh(1.0))
  inspect(cube.vertices[6][0], content="2")
}