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 用の関数に 4 成分のベクトルを渡すと最初の 3 成分として扱われます。

vec3

vec3 は 3 成分ベクトル (x,y,z)(x, y, z) を作ります。

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

vec4

vec4 は 4 成分ベクトル (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) は 3 次元の差 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 は 3 つの数の最小値と最大値を返します。ラスタライザは三角形のバウンディングボックスにこれらを使います。

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 はメッシュの 1 つの面で、頂点配列への 4 つのインデックスで表されます。

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 は 3 つの頂点インデックスで表される三角形で、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) は face の 4 頂点を a、b、c、d の順に返します。

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]

どちらの三角形も四角形の巡回順を保ちます。退化した四角形では 2 つ目の三角形 (a,c,a)(a, c, a) の面積が 0 になり、ラスタライザはそれをスキップします。

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) は面の 4 頂点の平均 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]

退化した四角形では、重複した頂点が 2 回数えられます。

face_normal

face_normal(vertices, face) は最初の 3 頂点から計算した単位法線 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]

3 頂点が同一直線上にあるときは零ベクトルを返します。

face_is_visible

face_is_visible(vertices, face, eye) は背面判定で、cc を面の中心として n^⋅(e−c)>0\hat n \cdot (e - c) > 0 のとき true を返します。

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

行列

行列の構築関数は列ベクトル (x,y,z,w)T(x, y, z, w)^\mathsf{T} に作用する 4×4 の @la.Matrix[Double] を返します。角度の単位はラジアンです。

identity_matrix4

identity_matrix4() は 4×4 の単位行列 I4I_4 を返します。

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

translation_matrix

translation_matrix(x, y, z) は t=(x,y,z)Tt = (x, y, z)^\mathsf{T} として (I3t01)\begin{pmatrix} I_3 & t \\ 0 & 1 \end{pmatrix} を返します。

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 の同次行列を 1 つ包み、それを点、方向、メッシュに適用します。

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) は、最終行が (0,0,0,1)(0, 0, 0, 1) でない射影行列を含め、任意の 4×4 行列を包みます。

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) は点 pp を 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} で写し、(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) は方向 dd を w=0w = 0 で写すので平行移動の影響を受けず、除算なしで最初の 3 成分を返します。

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