Huli's Blog.

InsetFace 内插面算法实现

Huli
Huli

InsetFace 内插面算法实现——从二维 Offset 到三维区域求解

在建模软件里,Inset Face 看起来是一个很小的操作:选中一个面,拖一下厚度,轮廓往里缩一圈;如果再给一点 Depth,它就变成一个凹进去或者凸出来的结构。

但是,如果仅把 inset 理解成把所有顶点朝中心点拉一拉,结果基本不会太理想。

这里记录的是我实现 InsetFace 插件时整理出来的一套做法。它不是一个学术级的 mesh offset 系统,更像是面向交互式建模工具的工程实现:常见情况尽量正确,复杂情况给出可控近似,出问题时优先保证预览不炸。整体思路参考了 Blender、Maya 这类现代 3D 软件里的 inset 行为,我是做法仅供参考,如果要做生产级实现,还是建议直接去看这些软件本身的代码和数据结构设计。


一、问题怎么拆

如果只处理一个平面多边形,inset 的定义其实很清楚:每条边向内部平移一段距离,然后相邻平移边求交,得到新的内轮廓。

麻烦在于建模软件里的面不是孤立的二维多边形。它们在三维空间里,可能多个面连成一个区域,区域还可能不是共面的。Thickness 到底表示顶点移动距离,还是边到边的真实距离,也会影响最后的算法分支。

我最后把实现拆成了三层:

  1. 单面 inset
    把 3D 面投影到局部 2D 平面,在 2D 里做 polygon offset,再抬回 3D。
  2. 普通区域 inset
    每个面先做局部 inset,然后把共享顶点的切向位移做加权融合。
  3. 区域 EvenOffset inset
    如果区域近似共面,就做统一的 2D 区域 offset;如果明显非共面,就走曲面感知的 3D 切向近似。

所以这个插件的核心大概可以写成:

InsetFace=2D polygon offset+local frame projection+half-edge boundary extraction+tangent displacement smoothing\normalsize \text{InsetFace} = \text{2D polygon offset} + \text{local frame projection} + \text{half-edge boundary extraction} + \text{tangent displacement smoothing}

这几个部分听起来分散,但实现里基本就是围绕这条线组织的。


二、先把问题拉回二维

3D - 2D Offset - 3D
3D - 2D Offset - 3D

单个面 inset 最适合先变成 2D 问题。给定面顶点:

x0,x1,,xn1\normalsize \mathbf{x}_0, \mathbf{x}_1, \dots, \mathbf{x}_{n-1}

先计算面法线。我这里用的是 Newell 风格的多边形法线累积:

nx+=(yiyi+1)(zi+zi+1)\normalsize n_x += (y_i - y_{i+1})(z_i + z_{i+1})
ny+=(zizi+1)(xi+xi+1)\normalsize n_y += (z_i - z_{i+1})(x_i + x_{i+1})
nz+=(xixi+1)(yi+yi+1)\normalsize n_z += (x_i - x_{i+1})(y_i + y_{i+1})

然后归一化:

n^=nn\normalsize \widehat{\mathbf{n}} = \frac{\mathbf{n}}{\|\mathbf{n}\|}

局部坐标系用面上的第一个点作为原点:

o=x0\normalsize \mathbf{o} = \mathbf{x}_0

再找一条有效边作为 u\normalsize u 方向:

u=x1x0x1x0\normalsize \mathbf{u} = \frac{\mathbf{x}_1 - \mathbf{x}_0} {\|\mathbf{x}_1 - \mathbf{x}_0\|}

第二个基向量由法线叉乘得到:

v=n^×u\normalsize \mathbf{v} = \widehat{\mathbf{n}} \times \mathbf{u}

这样三维点 x\normalsize \mathbf{x} 就可以投影成二维坐标:

q=[(xo)u(xo)v]\normalsize \mathbf{q} = \begin{bmatrix} (\mathbf{x} - \mathbf{o}) \cdot \mathbf{u} \\ (\mathbf{x} - \mathbf{o}) \cdot \mathbf{v} \end{bmatrix}

二维 offset 得到新点 q\normalsize \mathbf{q}' 后,再回到 3D:

x=o+qxu+qyv+dn^\normalsize \mathbf{x}' = \mathbf{o} + q'_x \mathbf{u} + q'_y \mathbf{v} + d\widehat{\mathbf{n}}

这里的 d\normalsize d 对应用户输入的 Depth

单面流程可以概括成:

TEXT

3D 多边形
→ 计算局部平面
→ 投影到 2D
→ 2D offset
→ 反投影回 3D
→ 沿法线加 Depth

这个路径后面会反复用到。区域算法看起来更复杂,但底层依然依赖稳定的局部 2D offset。


三、OffsetLoop2D

这里是真正实现轮廓偏移的地方。

OffsetLoop2D() 是整个实现里最核心的函数。输入是二维闭合多边形:

P={p0,p1,,pn1}\normalsize P = \{\mathbf{p}_0, \mathbf{p}_1, \dots, \mathbf{p}_{n-1}\}

输出是内缩后的多边形:

P={p0,p1,,pn1}\normalsize P' = \{\mathbf{p}'_0, \mathbf{p}'_1, \dots, \mathbf{p}'_{n-1}\}

首先要判断多边形方向。二维有向面积为:

A(P)=12i=0n1cross(pi,pi+1)\normalsize A(P) = \frac{1}{2} \sum_{i=0}^{n-1} \operatorname{cross}(\mathbf{p}_i, \mathbf{p}_{i+1})

其中:

cross(a,b)=axbyaybx\normalsize \operatorname{cross}(\mathbf{a}, \mathbf{b}) = a_x b_y - a_y b_x

如果 A(P)>0\normalsize A(P) > 0,多边形是逆时针;如果 A(P)<0\normalsize A(P) < 0,则是顺时针。

为了后面统一处理,我用了一个方向符号:

s={1,A(P)01,A(P)<0\normalsize s = \begin{cases} 1, & A(P) \ge 0 \\ -1, & A(P) < 0 \end{cases}

对当前顶点 pi\normalsize \mathbf{p}_i,取前后两条边的单位方向:

d0=pipi1pipi1\normalsize \mathbf{d}_0 = \frac{\mathbf{p}_i - \mathbf{p}_{i-1}} {\|\mathbf{p}_i - \mathbf{p}_{i-1}\|}
d1=pi+1pipi+1pi\normalsize \mathbf{d}_1 = \frac{\mathbf{p}_{i+1} - \mathbf{p}_i} {\|\mathbf{p}_{i+1} - \mathbf{p}_i\|}

二维左法向定义为:

perpLeft(x,y)=(y,x)\normalsize \operatorname{perpLeft}(x,y)=(-y,x)

于是两条边的 inward normal 写成:

n0=sperpLeft(d0)\normalsize \mathbf{n}_0 = s \cdot \operatorname{perpLeft}(\mathbf{d}_0)
n1=sperpLeft(d1)\normalsize \mathbf{n}_1 = s \cdot \operatorname{perpLeft}(\mathbf{d}_1)

接下来分两种情况:普通 inset 和 Offset Even


四、普通 Inset

不开启 Offset Even 时,我用的是比较轻的方案:顶点沿相邻两条边 inward normal 的角平分方向移动。

角平分方向为:

b=n0+n1n0+n1\normalsize \mathbf{b} = \frac{\mathbf{n}_0 + \mathbf{n}_1} {\|\mathbf{n}_0 + \mathbf{n}_1\|}

新顶点为:

pi=pi+tb\normalsize \mathbf{p}'_i = \mathbf{p}_i + t\mathbf{b}

这里 t\normalsize tThickness

这个模式下,Thickness 更接近“顶点沿角平分线移动的距离”,不是严格的边到边距离。

如果该顶点的内角是 θ\normalsize \theta,实际边距大致是:

dedgetcosθ2\normalsize d_{\text{edge}} \approx t\cos\frac{\theta}{2}

所以钝角处的实际边距会比输入厚度小,锐角处则会显得收缩更明显。这也是普通 inset 和 Offset Even 看起来不一样的原因。

这条路径的价值在于简单、快,而且稳定。作为交互式工具的默认轻量分支,它比较合适。


五、Offset Even

开启 Offset Even 后,Thickness 应该更接近真实的边距。这个时候不能只推顶点,而要平移边线。

Regular Inset vs Offset Even
Regular Inset vs Offset Even

对当前顶点 pi\normalsize \mathbf{p}_i,相邻两条边分别沿 inward normal 平移 t\normalsize t,得到两条偏移线:

L0(α)=(pi+tn0)+αd0\normalsize L_0(\alpha) = (\mathbf{p}_i + t\mathbf{n}_0) + \alpha\mathbf{d}_0
L1(β)=(pi+tn1)+βd1\normalsize L_1(\beta) = (\mathbf{p}_i + t\mathbf{n}_1) + \beta\mathbf{d}_1

新顶点是这两条线的交点:

(pi+tn0)+αd0=(pi+tn1)+βd1\normalsize (\mathbf{p}_i + t\mathbf{n}_0) + \alpha\mathbf{d}_0 = (\mathbf{p}_i + t\mathbf{n}_1) + \beta\mathbf{d}_1

这个点满足:

dist(pi,Ei1)t\normalsize \operatorname{dist}(\mathbf{p}'_i, E_{i-1}) \approx t
dist(pi,Ei)t\normalsize \operatorname{dist}(\mathbf{p}'_i, E_i) \approx t

也就是说,Offset Even 的语义更接近传统建模软件里的等距 inset。

但它也更容易遇到数值问题。两条边接近平行时,交点会跑得很远;角很尖时,miter 也可能被拉成一根长刺。所以这里必须加限制。


六、Miter Limit

Offset 里的尖角很麻烦。两条偏移线夹角太小时,求出来的交点可能离原顶点非常远,形成一个夸张的 miter。

我的处理是:如果直接求交失败,或者结果不稳定,就退回到角平分线方向。

角平分方向仍然是:

b=n0+n1n0+n1\normalsize \mathbf{b} = \frac{\mathbf{n}_0 + \mathbf{n}_1} {\|\mathbf{n}_0 + \mathbf{n}_1\|}

理论上的 miter 长度为:

=tbn1\normalsize \ell = \frac{t}{\mathbf{b}\cdot \mathbf{n}_1}

候选点为:

pi=pi+b\normalsize \mathbf{p}'_i = \mathbf{p}_i + \ell \mathbf{b}

但实际实现里会限制长度:

tM\normalsize |\ell| \le |t|M

普通模式使用:

M=8.0\normalsize M = 8.0

Offset Even 模式使用:

M=12.0\normalsize M = 12.0

这两个数不是推导出来的,是工程里调出来的。太小会把尖角削得很平,太大又容易把预览拉飞。现在这个范围能保留一定锐利感,也不会让单个极端角把整个轮廓弄坏。


七、Offset 结果要做合法性检查

二维 offset 得到的候选轮廓不一定可用。厚度过大时,多边形可能塌缩、自交,甚至方向翻转。凹多边形和窄长面尤其容易出问题。

所以 OffsetLoop2D() 生成候选轮廓后,我会做几类检查。

面积不能接近零:

A(P)>ε\normalsize |A(P')| > \varepsilon

方向不能翻转:

sign(A(P))=sign(A(P))\normalsize \operatorname{sign}(A(P')) = \operatorname{sign}(A(P))

同时还要检查不能自交。

如果检查失败,我没有让它直接报错,而是缩小厚度后重试,最多尝试 8 次:

tk+1=0.5tk\normalsize t_{k+1} = 0.5t_k

八、Individual 模式

有了单面 inset,Individual 模式就比较直接,每个面独立内插即可。每个选中面独立执行:

TEXT

取面顶点
→ 计算面法线
→ 建立局部 2D 坐标
→ OffsetLoop2D
→ 抬回 3D
→ 生成内盖和侧壁

对面 f\normalsize f 中的顶点 i\normalsize i,最终位置可以写成:

xf,i=of+qf,i,xuf+qf,i,yvf+dn^f\normalsize \mathbf{x}'_{f,i} = \mathbf{o}_f + q'_{f,i,x}\mathbf{u}_f + q'_{f,i,y}\mathbf{v}_f + d\widehat{\mathbf{n}}_f

这个模式局部稳定,特别适合多个不共面的独立面。

它的限制也很直接:两个相邻面如果共享一条边,各自独立 inset 后,新边不一定协调。区域模式就是为了解决这个问题做的。


九、Region 普通模式

Region 模式下,如果没有开启 Offset Even,我没有直接把整个区域压到一个平面里做 offset。原因很简单:区域一旦不是共面,投影平面上的距离就不再可靠,做出来的 inset 会带明显变形。

这里采用的做法是:每个面先局部 inset,得到局部位移;然后把这些位移投影到顶点切平面,再对共享顶点做加权融合。

对面 f\normalsize f 中的顶点 v\normalsize v,局部 inset 给出:

xf,vinset\normalsize \mathbf{x}^{inset}_{f,v}

局部位移为:

Δxf,vlocal=xf,vinsetxv\normalsize \Delta \mathbf{x}^{local}_{f,v} = \mathbf{x}^{inset}_{f,v} - \mathbf{x}_v

由于不同面法线不一样,这个位移里可能混有不一致的法向分量。于是先投影到顶点平均法线对应的切平面。

设顶点平均法线为:

n^v\normalsize \widehat{\mathbf{n}}_v

切向位移为:

Δxf,vtan=Δxf,vlocal(Δxf,vlocaln^v)n^v\normalsize \Delta \mathbf{x}^{tan}_{f,v} = \Delta \mathbf{x}^{local}_{f,v} - \left( \Delta \mathbf{x}^{local}_{f,v} \cdot \widehat{\mathbf{n}}_v \right) \widehat{\mathbf{n}}_v

权重用顶点处的角度。某个面在这个顶点占的角越大,它对最终位移的贡献也应该越大:

wf,v(eprev,enext)\normalsize w_{f,v} \approx \angle(\mathbf{e}_{prev}, \mathbf{e}_{next})

最终共享顶点的切向位移为:

Δxvtan=fwf,vΔxf,vtanfwf,v\normalsize \Delta \mathbf{x}^{tan}_v = \frac{ \sum_f w_{f,v} \Delta \mathbf{x}^{tan}_{f,v} }{ \sum_f w_{f,v} }

新位置为:

xv=xv+Δxvtan+dn^v\normalsize \mathbf{x}'_v = \mathbf{x}_v + \Delta \mathbf{x}^{tan}_v + d\widehat{\mathbf{n}}_v

这个方法不是严格的区域 offset。它做的是在每个顶点的切空间里融合局部 inset 结果。精度上有妥协,但稳定性不错,尤其适合普通区域 inset 的交互预览。


十、Region + Offset Even

Region + EvenOffset 是最绕的一条分支。

我早期版本这里处理得比较简单:只要用户开启均等偏移,就把整个选中区域投影到一个统一的 2D 平面里,算完 offset 后再抬回 3D。平面模型上这套方案看着没什么问题,轮廓也干净,所以一开始我没有特别想过它。

问题是插件发给别人试用后才暴露的。他拿了一个带折角的区域测试,结果折角附近的 inset 边界偏到了不该去的位置,侧壁也开始出现不协调的扭曲。下面这张图就是当时的效果。

inset bug

这个 bug 的根源不在 OffsetLoop2D()。2D 里的等距偏移本身没有算错,真正的问题是统一投影这一步把折角处的局部切空间信息抹掉了。

早期做法相当于把所有切向位移都限制在同一个区域平面 Tregion\normalsize T_{region} 上:

Δxi2D=(qi,xqi,x)u+(qi,yqi,y)v\normalsize \Delta \mathbf{x}^{2D}_i = (q'_{i,x}-q_{i,x})\mathbf{u} + (q'_{i,y}-q_{i,y})\mathbf{v}

这个位移满足的是区域平面上的 offset 语义。可是在折角处,同一个共享顶点周围会有多个局部面,它们的切平面并不一致。先把这些面压到同一个 2D 平面里,再把结果抬回去,本质上就是用一个全局切平面替代了局部切空间。

这里还有一个容易误判的点:即使 Depth 最后仍然沿顶点平均法线加上去,问题也不会自动消失。因为 Thickness 对应的切向收缩方向已经在投影阶段被改写了。最后看到的侧壁扭曲,其实是前面边界目标位置就已经偏了。

所以后来我把 Region + Offset Even 拆成现在的两条路径:

  • 近共面区域继续走统一 2D offset,因为这时投影误差还在可控范围内;
  • 非共面区域不再强行压平,而是先用局部面 even inset 生成边界目标,再把位移投影到顶点切平面里传播。

这也是后面 nearly planar 判断存在的原因。它不是为了让流程更复杂,而是为了避免在折角、弯曲区域里误用单一投影平面。这个坑也提醒我,建模操作里很多“看起来可以统一成 2D 问题”的地方,最好先检查一下局部切空间有没有被一起丢掉。


十一、区域平面估计

区域中心取所有顶点的平均:

c=1Ni=1Nxi\normalsize \mathbf{c} = \frac{1}{N} \sum_{i=1}^{N} \mathbf{x}_i

区域法线用面积加权的面法线平均:

n=fAfn^f\normalsize \mathbf{n} = \sum_f A_f \widehat{\mathbf{n}}_f

归一化:

n^=nn\normalsize \widehat{\mathbf{n}} = \frac{\mathbf{n}}{\|\mathbf{n}\|}

然后从区域中找最长边方向,投影到该平面作为 u\normalsize \mathbf{u},再计算:

v=n^×u\normalsize \mathbf{v} = \widehat{\mathbf{n}} \times \mathbf{u}

这样就得到区域级的局部坐标系。


十二、近共面判定

我用了两个指标判断区域是否足够接近平面:

  1. 顶点到区域平面的最大距离;
  2. 面法线和区域法线的最大夹角。

设有效厚度为:

teff=max(t,104)\normalsize t_{eff} = \max(|t|, 10^{-4})

只有同时满足:

dmax0.25teff\normalsize d_{max} \le 0.25t_{eff}

以及:

θmax8\normalsize \theta_{max} \le 8^\circ

才认为它是 nearly planar。

这个阈值也是偏工程的选择。直觉上,如果区域起伏相对 inset 厚度很小,把它压到一个 2D 平面里求 offset 是可以接受的。反过来,如果区域本身已经明显弯曲,统一投影就会制造更大的误差。


十三、近共面区域

整体思路为边界 Offset + Laplace 插值。

对 near planar 区域,先把所有区域顶点投影到统一 2D 平面:

qi=[(xic)u(xic)v]\normalsize \mathbf{q}_i = \begin{bmatrix} (\mathbf{x}_i - \mathbf{c}) \cdot \mathbf{u} \\ (\mathbf{x}_i - \mathbf{c}) \cdot \mathbf{v} \end{bmatrix}

然后用半边结构提取区域边界。

半边结构做这件事很顺手:如果一条半边在选中区域内部有 pair,它就是内部边;如果没有对应的区域内 pair,它就在区域边界上。沿这些边界半边继续追踪,就能得到一个或多个闭合边界环。

这里要处理孔洞。外边界和孔洞的 offset 方向相反:

对外边界:

tloop=t\normalsize t_{loop} = t

对孔洞:

tloop=t\normalsize t_{loop} = -t

然后每个边界环调用:

TEXT

OffsetLoop2D(loop, t_loop, true)

边界点有了目标位置后,内部点还要跟着移动。当前实现里,我固定边界,对内部点做邻接平均迭代。对内部点 i\normalsize i

qi(k+1)=1N(i)jN(i)qj(k)\normalsize \mathbf{q}^{(k+1)}_i = \frac{1}{|\mathcal{N}(i)|} \sum_{j\in\mathcal{N}(i)} \mathbf{q}^{(k)}_j

边界点固定不动,内部点迭代更新。

这相当于近似求解离散 Laplace 方程:

Δq=0\normalsize \Delta \mathbf{q} = 0

也就是 harmonic interpolation。

这里没有显式构造稀疏矩阵,而是用了固定次数的 Jacobi 型迭代。它不是最高效、最严格的线性系统解法,但实现简单,预览阶段也足够稳。

得到新的 2D 位置后,再回到 3D:

xi=c+qi,xu+qi,yv+dn^i\normalsize \mathbf{x}'_i = \mathbf{c} + q'_{i,x}\mathbf{u} + q'_{i,y}\mathbf{v} + d\widehat{\mathbf{n}}_i

Depth 这里用的是顶点平均法线 n^i\normalsize \widehat{\mathbf{n}}_i,不是统一的区域法线。这样轻微弯曲的区域看起来会自然一些。


十四、非共面区域

如果区域不满足近共面条件,采用曲面感知的近似路径,不再把它硬投影到一个平面里。

这条路径的思路是:边界点由局部面 even inset 结果给出,内部点通过 3D 切向平滑传播。

先对每个相邻面做局部 even inset,得到边界顶点在每个面里的局部 inset 位置:

xf,vinset\normalsize \mathbf{x}^{inset}_{f,v}

局部位移为:

Δxf,vlocal=xf,vinsetxv\normalsize \Delta \mathbf{x}^{local}_{f,v} = \mathbf{x}^{inset}_{f,v} - \mathbf{x}_v

然后投影到顶点切平面:

Δxf,vtan=Δxf,vlocal(Δxf,vlocaln^v)n^v\normalsize \Delta \mathbf{x}^{tan}_{f,v} = \Delta \mathbf{x}^{local}_{f,v} - \left( \Delta \mathbf{x}^{local}_{f,v} \cdot \widehat{\mathbf{n}}_v \right) \widehat{\mathbf{n}}_v

再用角度权重融合,得到边界目标位移:

Δxvboundary=fwf,vΔxf,vtanfwf,v\normalsize \Delta \mathbf{x}^{boundary}_v = \frac{ \sum_f w_{f,v} \Delta \mathbf{x}^{tan}_{f,v} }{ \sum_f w_{f,v} }

边界位移固定后,内部点在 3D 里迭代传播位移。

对内部顶点 i\normalsize i

Δxi(k+1)=ΠTi(1N(i)jN(i)Δxj(k))\normalsize \Delta \mathbf{x}^{(k+1)}_i = \Pi_{T_i} \left( \frac{1}{|\mathcal{N}(i)|} \sum_{j\in\mathcal{N}(i)} \Delta \mathbf{x}^{(k)}_j \right)

其中 ΠTi\normalsize \Pi_{T_i} 表示投影到顶点 i\normalsize i 的切平面:

ΠTi(a)=a(an^i)n^i\normalsize \Pi_{T_i}(\mathbf{a}) = \mathbf{a} - (\mathbf{a}\cdot \widehat{\mathbf{n}}_i) \widehat{\mathbf{n}}_i

最终位置为:

xi=xi+Δxitan+dn^i\normalsize \mathbf{x}'_i = \mathbf{x}_i + \Delta \mathbf{x}^{tan}_i + d\widehat{\mathbf{n}}_i

这不是严格的 geodesic offset。严格曲面 offset 要处理测地距离、曲率、局部参数化和拓扑变化,在任意三角网格上会复杂很多。

但作为建模插件里的交互路径,这个近似比较实用:它不会因为区域轻微弯曲就失效,也不会把所有点强行压到一个平面里。当前实现里,如果这条曲面感知路径失败,会回退到普通 patch inset,而不是直接报错。


十五、预览和提交分开

这个插件里还有一个比较重要的工程结构:预览和提交分离。

预览阶段不会直接改原始网格,而是构建临时结构 TempRegion,里面保存:

  • 临时顶点;
  • 临时半边;
  • 临时面;
  • 边界环;
  • 区域平面;
  • inset 后的目标位置。

预览只生成可视化线条,比如新内轮廓边、原边界点到新边界点的连接线。

这个设计很实际。用户拖动参数时,插件可能一秒钟重建很多次结果。如果每次都直接写回真实拓扑,撤销、回滚、法线更新、坏几何清理都会变得麻烦。临时结构让预览轻很多,也能避免中间状态污染原始网格。

提交阶段才真正写回拓扑,大致流程是:

  1. 创建 inset 后的新顶点;
  2. 用新顶点生成内盖面;
  3. 用原边和新边生成侧壁四边形;
  4. 删除原始选中面;
  5. compact 网格;
  6. 更新法线。

侧壁四边形的顶点顺序是:

[xstartorig,xendorig,xendnew,xstartnew]\normalsize [ \mathbf{x}^{orig}_{start}, \mathbf{x}^{orig}_{end}, \mathbf{x}^{new}_{end}, \mathbf{x}^{new}_{start} ]

这样可以自然连接原边界和新内边界,同时保持面朝向一致。


十六、严格性和交互稳定性的取舍

写这类工具时经常会遇到一个 trade-off:到底要追求几何上更严格的 offset,还是先保证交互过程稳定、快速、可预期。

当前实现里有不少 trade-off:

  • 内部点使用固定次数邻接平均,而不是精确稀疏线性系统;
  • 非共面 Offset Even 是曲面感知近似,不是严格测地 offset;
  • UV 目前只做轻量处理,没有重新做完整参数化;
  • 自交处理采用缩小厚度重试,而不是完整 polygon clipping。

不用说,这些确实是目前插件的限制。但对这个插件来说,首先面对的是交互场景。用户拖动参数时算法要快;普通模型上结果要稳定;遇到局部坏几何时最好有 fallback;一个极端顶点不应该让整个操作崩掉。

所以它更像一个工程化的 inset solver,而不是一个完整的 polygon offset 或 mesh parameterization 系统。近似是存在的,但尽量放在能解释、能控制的位置。


十七、整体流程

InsetFace 的决策结构大致如下:

TEXT

InsetFace
├── Individual
│ └── 每个面独立:
│ 3D 面 → 局部 2D → OffsetLoop2D → 回到 3D
└── Region
├── EvenOffset = false
│ └── 每面局部 inset
│ → 位移投影到顶点切平面
│ → 角度加权平均
└── EvenOffset = true
├── nearly planar
│ └── 区域统一投影到 2D
│ → 提取边界环
│ → 边界 even offset
│ → 内部 Laplace 平滑
│ → 回到 3D
└── nonplanar
└── 局部面 inset 生成边界目标
→ 3D 切向位移平滑
→ 加 Depth

More Stories

MTR-Dev Webinar

NTE train JS discussion & sharing

Huli
Huli

在博客中实现 LaTeX 公式支持

在 Next.js + Sanity 的博客中实现 LaTeX 公式支持

Huli
Huli