///|
/// 网格化选项。
pub(all) struct MeshOptions {
/// 最长轴上的采样分段数(不是每轴都切这么多段)。
resolution : Int32
/// 是否把交点沿梯度投到零等值面。开着头顶点更贴近曲面;
/// 关掉可用于比较「阶梯误差」与「投影残差」。
project : Bool
/// Newton 迭代次数。2 次足够把残差压到体素尺度的三阶。
iterations : Int32
}
///|
pub fn MeshOptions::new(resolution : Int32) -> MeshOptions {
{ resolution, project: true, iterations: 2, }
}
///|
/// 立方体沿主对角线 `(0,0,0)-(1,1,1)` 拆成 6 个四面体。
///
/// 对角方式固定成这一种:换一种拆分,同一个 SDF 在同一个分辨率下会得到
/// 不同的三角数,体积也就不可比。这条写进 ARCHITECTURE。
let tet_table : Array[Array[Int32]] = [
[0, 1, 3, 7],
[0, 1, 5, 7],
[0, 2, 3, 7],
[0, 2, 6, 7],
[0, 4, 5, 7],
[0, 4, 6, 7],
]
///|
fn ceil_int(v : Double) -> Int32 {
let i = v.to_int()
if i.to_double() < v {
i + 1
} else {
i
}
}
///|
fn max_component(v : Vec3) -> Double {
let a = if v.x > v.y { v.x } else { v.y }
if a > v.z {
a
} else {
v.z
}
}
///|
/// 网格构建期的可变状态。没有用闭包,是因为闭包捕获可变数组在
/// 不同后端上的行为不该成为几何结果的一部分——显式传 `self` 更稳。
priv struct Builder {
sdf : Sdf
positions : Array[Vec3]
/// 每个顶点处的外法线(SDF 梯度)。逐面定绕向时用它当参考:
/// 梯度就在面上,比「拿某个格点当外部参考点」稳——格点可能恰好
/// 贴着等值面,参考方向会被浮点噪声决定。
normals : Array[Vec3]
seen : @hashmap.HashMap[UInt64, Int32]
project : Bool
iterations : Int32
/// 格点总数,用于把「格点索引对」编码成唯一键。
slots : UInt64
}
///|
fn Builder::new(
sdf : Sdf,
project : Bool,
iterations : Int32,
slots : UInt64,
) -> Builder {
{
sdf,
positions: [],
normals: [],
seen: @hashmap.HashMap([], capacity=1024),
project,
iterations,
slots,
}
}
///|
/// 取(或新建)一条四面体边上的交点。
///
/// 键是两个**格点索引**的 `min/max`:同一条几何边无论从哪个四面体、
/// 哪个方向被访问,算出的键相同,于是天然焊接,不需要 eps 量化。
/// 位置也统一按 `min -> max` 方向插值,避免「同一键两个浮点值」。
fn Builder::edge_vertex(
self : Builder,
i : Int64,
j : Int64,
pi : Vec3,
fi : Double,
pj : Vec3,
fj : Double,
) -> Int32 {
let a = if i < j { i } else { j }
let b = if i < j { j } else { i }
let key = a.reinterpret_as_uint64() * self.slots + b.reinterpret_as_uint64()
match self.seen.get(key) {
Some(idx) => idx
None => {
let pa = if i < j { pi } else { pj }
let fa = if i < j { fi } else { fj }
let pb = if i < j { pj } else { pi }
let fb = if i < j { fj } else { fi }
let mut t = if fa == fb { 0.5 } else { fa / (fa - fb) }
// 端点恰好落在零等值面时 t 会贴到 0 或 1,产生零面积三角形。
// 夹一个相对体素尺度的极小量,既不改变插值方向也不会漏面。
if t < 1.0e-6 {
t = 1.0e-6
}
if t > 1.0 - 1.0e-6 {
t = 1.0 - 1.0e-6
}
let mut p = pa.add(pb.sub(pa).mul(t))
if self.project {
let mut k = 0
while k < self.iterations {
let d = self.sdf.eval(p)
let g = self.sdf.gradient(p)
let gl = g.length()
if gl <= 1.0e-12 {
k = self.iterations
} else {
// 沿单位梯度走 `d`:对精确 SDF 一步到位,对 BOX 棱边附近
// |∇f| 偏离 1 的区域也不会过冲(牛顿步会)。
p = p.sub(g.div(gl).mul(d))
k = k + 1
}
}
}
let idx = self.positions.length()
self.positions.push(p)
self.normals.push(self.sdf.gradient(p))
self.seen.set(key, idx)
idx
}
}
}
///|
/// 追加一个三角形,并按「法线背离内部」统一绕向。
///
/// 不能只靠最后的体积符号整体翻转:同一份网格里若一部分面朝外、
/// 一部分朝内,散度体积会互相抵消,看起来只是「体积偏小」而不报错。
/// 这里用「参考点 `ref` 是已知外部点」逐面定绕向:法线应指向 `ref` 一侧。
fn Builder::emit(
self : Builder,
out : Array[Int32],
a : Int32,
b : Int32,
c : Int32,
) -> Unit {
let pa = self.positions[a]
let pb = self.positions[b]
let pc = self.positions[c]
let n = pb.sub(pa).cross(pc.sub(pa))
let outward = self.normals[a].add(self.normals[b]).add(self.normals[c])
if n.dot(outward) < 0.0 {
out.push(a)
out.push(c)
out.push(b)
} else {
out.push(a)
out.push(b)
out.push(c)
}
}
///|
/// 从 SDF 生成水密三角网格。
///
/// 采样在包围盒内做规则体素网格,每个体素拆 6 个四面体,按四面体顶点的
/// 符号取出 0/1/2 个三角形,交点在格点边上插值、按边键焊接。
/// 输出朝向由散度体积统一校正:体积为负就整体翻转。
pub fn mesh(sdf : Sdf, bounds : Aabb, opts : MeshOptions) -> Mesh {
let res = if opts.resolution < 2 {
2
} else if opts.resolution > 512 {
512
} else {
opts.resolution
}
let size = bounds.size()
let longest = max_component(size)
if longest <= 0.0 {
return Mesh::empty()
}
let h = longest / res.to_double()
let nx = if ceil_int(size.x / h) < 1 { 1 } else { ceil_int(size.x / h) }
let ny = if ceil_int(size.y / h) < 1 { 1 } else { ceil_int(size.y / h) }
let nz = if ceil_int(size.z / h) < 1 { 1 } else { ceil_int(size.z / h) }
let nx1 = nx + 1
let ny1 = ny + 1
let nz1 = nz + 1
let slots = (nx1 * ny1 * nz1).to_uint64()
let field : Array[Double] = []
let mut zi = 0
while zi < nz1 {
let mut yi = 0
while yi < ny1 {
let mut xi = 0
while xi < nx1 {
let p = Vec3::new(
bounds.min.x + xi.to_double() * h,
bounds.min.y + yi.to_double() * h,
bounds.min.z + zi.to_double() * h,
)
field.push(sdf.eval(p))
xi = xi + 1
}
yi = yi + 1
}
zi = zi + 1
}
let g = Array::make(8, 0L)
let fv = Array::make(8, 0.0)
let pv = Array::make(8, Vec3::zero())
let ins = Array::make(4, 0)
let outs = Array::make(4, 0)
let b = Builder::new(sdf, opts.project, opts.iterations, slots)
let tris : Array[Int32] = []
let mut z = 0
while z < nz {
let mut y = 0
while y < ny {
let mut x = 0
while x < nx {
// 8 个角点:bit0 = x, bit1 = y, bit2 = z
let mut c = 0
while c < 8 {
let cx = x + (c & 1)
let cy = y + ((c >> 1) & 1)
let cz = z + ((c >> 2) & 1)
let slot = cx + nx1 * (cy + ny1 * cz)
g[c] = slot.to_int64()
fv[c] = field[slot]
pv[c] = Vec3::new(
bounds.min.x + cx.to_double() * h,
bounds.min.y + cy.to_double() * h,
bounds.min.z + cz.to_double() * h,
)
c = c + 1
}
let mut t = 0
while t < 6 {
let tet = tet_table[t]
let mut ni = 0
let mut no = 0
let mut k = 0
while k < 4 {
let slot = tet[k]
if fv[slot] < 0.0 {
ins[ni] = slot
ni = ni + 1
} else {
outs[no] = slot
no = no + 1
}
k = k + 1
}
if ni == 1 {
let a = ins[0]
let v0 = b.edge_vertex(
g[a],
g[outs[0]],
pv[a],
fv[a],
pv[outs[0]],
fv[outs[0]],
)
let v1 = b.edge_vertex(
g[a],
g[outs[1]],
pv[a],
fv[a],
pv[outs[1]],
fv[outs[1]],
)
let v2 = b.edge_vertex(
g[a],
g[outs[2]],
pv[a],
fv[a],
pv[outs[2]],
fv[outs[2]],
)
b.emit(tris, v0, v1, v2)
} else if ni == 3 {
let o = outs[0]
let v0 = b.edge_vertex(
g[o],
g[ins[0]],
pv[o],
fv[o],
pv[ins[0]],
fv[ins[0]],
)
let v1 = b.edge_vertex(
g[o],
g[ins[1]],
pv[o],
fv[o],
pv[ins[1]],
fv[ins[1]],
)
let v2 = b.edge_vertex(
g[o],
g[ins[2]],
pv[o],
fv[o],
pv[ins[2]],
fv[ins[2]],
)
b.emit(tris, v0, v1, v2)
} else if ni == 2 {
let p = ins[0]
let q = ins[1]
let a = outs[0]
let d = outs[1]
let v0 = b.edge_vertex(g[p], g[a], pv[p], fv[p], pv[a], fv[a])
let v1 = b.edge_vertex(g[p], g[d], pv[p], fv[p], pv[d], fv[d])
let v2 = b.edge_vertex(g[q], g[d], pv[q], fv[q], pv[d], fv[d])
let v3 = b.edge_vertex(g[q], g[a], pv[q], fv[q], pv[a], fv[a])
b.emit(tris, v0, v1, v2)
b.emit(tris, v0, v2, v3)
}
t = t + 1
}
x = x + 1
}
y = y + 1
}
z = z + 1
}
let out = Mesh::new(b.positions, tris)
if out.is_empty() {
return out
}
if out.volume() < 0.0 {
out.flipped()
} else {
out
}
}