壳偏移(Shell Offset)是三维几何处理里非常基础又容易出问题的一类操作:给定一个封闭曲面,沿着法线方向把每个点推出去一段距离,得到一个新的曲面,两张曲面围成的空间就构成了壳体。这个操作看似简单,但一旦涉及大偏移距离、高曲率区域或非流形网格,就会出现自相交、翻转面片、厚度不均等各种毛病。本文围绕壳偏移的原理、常用算法以及厚度均匀化策略展开,帮你建立一个完整的处理思路。

壳偏移的数学原理与失效根源
壳偏移的形式化定义并不复杂。设原始曲面为 S,曲面上一点 p 的单位法向量为 n(p),偏移距离为 d,则偏移曲面上的对应点为 q = p + d · n(p)。向内偏移时 d 取负值。这个公式的隐患在于:它假设法线场是连续且不发生缠绕的,而真实网格的曲率会破坏这个假设。
考虑一个凹面区域。向内偏移时,凹面的法线会向中心汇聚,当偏移距离 d 大于局部凹面曲率半径 R 时,偏移点会越过汇聚点,产生自相交。表现在结果上就是模型内部出现翻折的"舌头"状几何,布尔运算和打印切片都会失败。反过来,向外偏移凸面时法线发散,一般不会自相交,但相邻面片的间距会拉大,网格稀疏处会出现缝隙。
所以判断一个区域能否安全偏移,核心条件是:内偏距离必须小于最小凹曲率半径。实践中可以先估算网格每个顶点的离散曲率(比如通过相邻面的法线夹角和边长加权),找出最小曲率半径,再确定安全的偏移上限。很多工程软件在你输入偏移量超限时提示失败,底层就是这类检测在起作用。
三种常见的壳偏移实现方案
第一种是直接法向偏移(direct normal offset),即逐顶点沿法线移动,保持面片拓扑不变。它的优点是实现简单、速度快,输出网格与输入一一对应。缺点是对网格质量极其敏感:如果原始网格法线方向不一致、顶点法线计算方式不统一(面积加权还是均值),偏移结果就会出现局部撕裂。此外它完全无法处理自相交,需要额外的求交检测和裁剪步骤。
第二种是基于符号距离场的隐式偏移。先把网格栅格化成体素,在每个体素中心采样符号距离(SDF),然后把等值面从 0 移动到 d,再用 Marching Cubes 重建出网格。这个方案天然免疫自相交问题,因为等值面提取阶段自动处理了汇聚区域,输出的永远是流形封闭曲面。代价是体素分辨率限制了细节精度,尖锐特征会被圆滑掉。下面是一个用 Python 体现思路的简化示例:
import numpy as np
from skimage import measure
def shell_offset_sdf(mesh, grid_size=128, distance=0.02):
# 计算网格包围盒并略微扩大,留出偏移空间
bbox_min, bbox_max = mesh.bounds
bbox_min -= distance * 2
bbox_max += distance * 2
# 生成采样网格
axes = [np.linspace(bbox_min[i], bbox_max[i], grid_size)
for i in range(3)]
xx, yy, zz = np.meshgrid(*axes, indexing='ij')
# 对每个采样点计算到网格的符号距离
# 实际实现常用 libigl 的 signed_distance,此处示意
points = np.stack([xx.ravel(), yy.ravel(), zz.ravel()], axis=1)
sdf_values = compute_signed_distance(mesh, points) # 外负内正或反之
# 提取距离等于目标偏移量的等值面
verts, faces, _, _ = measure.marching_cubes(
sdf_values.reshape(grid_size, grid_size, grid_size),
level=distance
)
return verts, faces
第三种是基于布尔运算的方案:先缩放或平移构造一个包围壳,再与原模型做布尔差。这种方式适合简单的凸体,对复杂拓扑的模型布尔运算本身就不稳定,一般只作为兜底手段。综合来看,SDF 方案在鲁棒性上优势明显,大多数商业软件的"加厚"功能底层都走这条路,只是在重建阶段会做特征保持的优化。
厚度均匀化的检测与修复
偏移完成后,壳体各处的实际厚度未必等于设定值。比如直接法向偏移在斜面区域,如果法线计算偏了,厚度就会沿区域漂移;SDF 方案在体素分辨率不足时,薄壁区域厚度会有半个体素量级的抖动。因此需要一个厚度分布检测步骤:对壳体外表面的每个顶点,沿内法线做射线求交,命中内表面的距离就是局部厚度。把厚度值映射成颜色可视化,能直观看出哪些区域偏薄。
厚度不达标时的修复思路有两种。一种是局部重新偏移:只对厚度异常区域的顶点重新计算法线并调整偏移量,做类似拉普拉斯的平滑迭代,让厚度场逐渐均匀。另一种是全局迭代投影:把内表面点云视为约束,反复执行"偏移-投影-平滑",直到厚度方差收敛。下面的伪代码展示了迭代投影的核心逻辑:
def equalize_thickness(outer_verts, inner_verts, target_t, iters=30):
for it in range(iters):
for i, p in enumerate(outer_verts):
# 找到内表面上距离 p 最近的点
q = nearest_point(inner_verts, p)
n = normalize(p - q) # 局部厚度方向
t = norm(p - q) # 当前局部厚度
# 按目标厚度与当前厚度的差值调整外表面点
correction = (target_t - t) * n
outer_verts[i] = p + correction * 0.5 # 阻尼因子防止震荡
# 平滑外表面,避免局部凸起
outer_verts = laplacian_smooth(outer_verts, strength=0.3)
if thickness_variance(outer_verts, inner_verts) < 1e-6:
break
return outer_verts
需要注意阻尼因子和平滑强度的权衡。阻尼太大收敛慢,太小会在薄壁区域来回震荡;平滑过度则会抹掉原本合理的细节特征。工程上通常先做几轮粗迭代,再降低步长做精修,同时把厚度检测的容差设置在工艺允许的范围内,比如3D打印场景给到喷嘴直径的一半左右,避免为了追求数学上的绝对均匀而过度处理。
实践中的经验总结
结合具体场景选择方案很重要。3D打印前的壳体化(掏空省料)通常偏移量在1到3毫米,SDF方案加0.05毫米左右的体素精度就够用,重建后记得做一次补洞和流形检查。模具设计中的偏移面要求保特征,倾向用直接法向偏移配合曲率自适应的网格细分,在拐角处加密顶点以减少失真。
另外几个常被忽略的细节:偏移前务必统一网格的法线方向并修复非流形边,否则SDF的正负号会算错,整个偏移方向就反了;偏移量超过最小凹曲率半径时,要么减小偏移量,要么先对原始曲面做局部圆角过渡,把曲率半径提上去再偏移;最后输出的壳体建议再做一次体积和壁厚的双重校验,这两项指标直接对应打印成败和结构强度,比肉眼检查网格外观可靠得多。