需求:基于初始三角3D网格的极小曲面近似算法(Python实现)
带约束的三角网格极小曲面近似算法(复刻Blender平滑顶点)
Blender的「平滑顶点」核心是带余切权重的拉普拉斯平滑,这个算法完全契合极小曲面的离散化准则(局部平均曲率为零),以下是核心逻辑、约束处理、收敛控制的细节,以及Python核心实现:
核心算法原理
极小曲面的离散化等价于让每个顶点的余切拉普拉斯向量为零——这个向量的计算考虑了相邻三角形的几何形状,比简单的邻域平均更贴合极小曲面的数学定义:
- 对每个非约束顶点 (v_i),拉普拉斯向量计算公式:
[
\Delta v_i = \frac{1}{2A_i} \sum_{j \in N(i)} (\cot \alpha_{ij} + \cot \beta_{ij}) (v_j - v_i)
]
其中:- (N(i)) 是 (v_i) 的邻接顶点集合
- (\alpha_{ij})、(\beta_{ij}) 是边 (v_i v_j) 两侧三角形中,与该边相对的夹角
- (A_i) 是 (v_i) 的Voronoi面积(可简化为相邻三角形面积的1/3)
- 顶点更新:(v_i^{t+1} = v_i^t + \lambda \cdot \Delta v_i),(\lambda) 是步长(松弛因子,通常取0.1~0.5)
约束处理
- 固定顶点:直接跳过更新,保留原始坐标
- 面内平滑:若需约束顶点在指定平面内,每次更新后将顶点投影回目标平面即可;也可在计算拉普拉斯向量时,仅保留平面内的分量
步长与收敛控制
- 步长 (\lambda):过小会导致收敛缓慢,过大可能引发震荡甚至发散,建议从0.2开始调试
- 收敛判定:两种常用方式:
- 计算所有顶点的位移平方和,当小于阈值(如 (1e-6))时停止迭代
- 固定迭代次数(Blender默认约10次,复杂网格可增加到50次)
Python核心实现(无依赖)
import numpy as np def build_cotangent_adjacency(vertices, faces): # 构建邻接表,存储每个顶点的邻接点及对应的余切权重 adjacency = {i: [] for i in range(len(vertices))} vertices = np.array(vertices) for face in faces: a, b, c = face # 计算当前面三条边的余切权重 # 边ab的余切(对应三角形中c角的邻边夹角) vec_ac = vertices[c] - vertices[a] vec_bc = vertices[c] - vertices[b] cross = np.cross(vec_ac, vec_bc) if np.linalg.norm(cross) < 1e-9: cot_ab = 0.0 else: cot_ab = np.dot(vec_ac, vec_bc) / np.linalg.norm(cross) # 边bc的余切(对应a角) vec_ba = vertices[a] - vertices[b] vec_ca = vertices[a] - vertices[c] cross = np.cross(vec_ba, vec_ca) if np.linalg.norm(cross) < 1e-9: cot_bc = 0.0 else: cot_bc = np.dot(vec_ba, vec_ca) / np.linalg.norm(cross) # 边ca的余切(对应b角) vec_cb = vertices[b] - vertices[c] vec_ab = vertices[b] - vertices[a] cross = np.cross(vec_cb, vec_ab) if np.linalg.norm(cross) < 1e-9: cot_ca = 0.0 else: cot_ca = np.dot(vec_cb, vec_ab) / np.linalg.norm(cross) # 更新邻接表(注意每条边属于两个面,完整实现需合并权重,这里简化为累加) adjacency[a].append((b, cot_ab)) adjacency[b].append((a, cot_ab)) adjacency[b].append((c, cot_bc)) adjacency[c].append((b, cot_bc)) adjacency[c].append((a, cot_ca)) adjacency[a].append((c, cot_ca)) return adjacency def constrained_laplacian_smooth(vertices, faces, fixed_vertex_indices, iterations=10, step=0.2, convergence_threshold=1e-6): vertices = np.array(vertices, dtype=np.float64) adjacency = build_cotangent_adjacency(vertices, faces) for _ in range(iterations): new_vertices = vertices.copy() total_displacement = 0.0 for idx in range(len(vertices)): if idx in fixed_vertex_indices: continue weight_sum = 0.0 laplacian = np.zeros(3) for neighbor_idx, cot_weight in adjacency[idx]: laplacian += cot_weight * (vertices[neighbor_idx] - vertices[idx]) weight_sum += cot_weight if weight_sum < 1e-9: continue # 归一化拉普拉斯向量,应用步长更新 laplacian /= weight_sum displacement = step * laplacian new_vertices[idx] += displacement total_displacement += np.linalg.norm(displacement) ** 2 # 检查收敛 if total_displacement < convergence_threshold: break vertices = new_vertices return vertices.tolist()
关键细节补充
- 余切权重的作用:权重大小对应相邻三角形的“平坦度”,平坦区域的权重更高,顶点移动时会更倾向于向这些区域靠拢,完美匹配极小曲面的平均曲率为零要求
- Blender的面内平滑:本质是将更新后的顶点投影到其相邻面的平均平面上,避免顶点被拉出原始网格的局部平面
- 简化版实现:如果不需要严格的极小曲面精度,也可以用简单邻域平均(每个顶点移动到邻接点的平均位置),但效果远不如余切权重版本贴合极小曲面准则
内容的提问来源于stack exchange,提问作者Franz
相关产品推荐
相关产品推荐

