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 成分ベクトル を作ります。
pub fn vec3(Double, Double, Double) -> @mutable.Vector[Double]
vec4
vec4 は 4 成分ベクトル を作ります。これは Transform3 の内部で使われる同次形式です。
pub fn vec4(Double, Double, Double, Double) -> @mutable.Vector[Double]
sub_vec
sub_vec(a, b) は 3 次元の差 を返します。
pub fn sub_vec(@mutable.Vector[Double], @mutable.Vector[Double]) -> @mutable.Vector[Double]
cross_vec
cross_vec(a, b) は外積 を返します。
pub fn cross_vec(@mutable.Vector[Double], @mutable.Vector[Double]) -> @mutable.Vector[Double]
vec_length
vec_length(v) は v の全成分にわたるユークリッドノルム を返します。
pub fn vec_length(@mutable.Vector[Double]) -> Double
成分の個数を返す @la.Vector::length と混同しないでください。
normalize_vec
normalize_vec(v) は を返し、 DEPTH_EPSILON のときは零ベクトル を返します。
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
}
頂点は が閉じたメッシュの外側を向くように並べます。三角形は最後のインデックスが最初と同じ()退化した四角形として格納します。
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) | 立方体 | 8 | 6 |
sphere_mesh(r, rings, segments) | 半径 の UV 球。極は 軸上 | ||
cylinder_mesh(r, h, n) | 軸に沿ったふた付きの円柱、 | ||
cone_mesh(r, h, n) | 底面付きの円錐。頂点は 、底面は | ||
triangular_pyramid_mesh(r, h) | 底面の三角形が半径 の円に内接する四面体 | 4 | 4 |
torus_mesh(R, r, M, 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
ふた、円錐の側面、角錐の面は退化した四角形()です。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) は四角形を扇形 、 に分割します。
pub fn triangulate_quad(QuadFace) -> Array[TriangleFace]
どちらの三角形も四角形の巡回順を保ちます。退化した四角形では 2 つ目の三角形 の面積が 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 頂点の平均 を返します。
pub fn face_center(Array[@mutable.Vector[Double]], QuadFace) -> @mutable.Vector[Double]
退化した四角形では、重複した頂点が 2 回数えられます。
face_normal
face_normal(vertices, face) は最初の 3 頂点から計算した単位法線 を返します。
pub fn face_normal(Array[@mutable.Vector[Double]], QuadFace) -> @mutable.Vector[Double]
3 頂点が同一直線上にあるときは零ベクトルを返します。
face_is_visible
face_is_visible(vertices, face, eye) は背面判定で、 を面の中心として のとき true を返します。
pub fn face_is_visible(Array[@mutable.Vector[Double]], QuadFace, @mutable.Vector[Double]) -> Bool
face_intensity
face_intensity(vertices, face, light) はランバート項 を返します。
pub fn face_intensity(Array[@mutable.Vector[Double]], QuadFace, @mutable.Vector[Double]) -> Double
light は表面から光源へ向かう単位ベクトルである必要があります。そのとき結果は入射角の余弦で、 に入ります。
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] を返します。角度の単位はラジアンです。
identity_matrix4
identity_matrix4() は 4×4 の単位行列 を返します。
pub fn identity_matrix4() -> @mutable.Matrix[Double]
translation_matrix
translation_matrix(x, y, z) は として を返します。
pub fn translation_matrix(Double, Double, Double) -> @mutable.Matrix[Double]
scale_matrix
scale_matrix(x, y, z) は を返します。
pub fn scale_matrix(Double, Double, Double) -> @mutable.Matrix[Double]
rotation_x, rotation_y, rotation_z
rotation_x(θ)、rotation_y(θ)、rotation_z(θ) は座標軸まわりに だけ回転する行列を返します。
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 ブロックは次のとおりです。
正の角度は、それぞれ を の方へ、 を の方へ、 を の方へ回します。
rotation_matrix
rotation_matrix(α, β, γ) はオイラー角による回転 を返します。
pub fn rotation_matrix(Double, Double, Double) -> @mutable.Matrix[Double]
列ベクトルに作用させると、まず まわり、次に 、最後に まわりに、すべて固定されたワールド座標軸について回転します。完全な行列とジンバルロックの振る舞いは 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) は、最終行が でない射影行列を含め、任意の 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) は点 を で写し、 を返します。
pub fn Transform3::apply_point(Self, @mutable.Vector[Double]) -> @mutable.Vector[Double]
アフィン変換では です。 DEPTH_EPSILON のときは除算を省き、 を返します。
Transform3::apply_direction
t.apply_direction(d) は方向 を で写すので平行移動の影響を受けず、除算なしで最初の 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")
}