简介:计算共形几何作业配套的C++项目框架,面向需要动手实现几何算法的本科生、研究生与开发者,也可作为计算共形几何课程的编程实践模板。压缩包内含373个文件,以295个C++头文件为核心,辅以17个MATLAB脚本、4个cpp实现、CMake配置及少量示例图片与文档,整体仅7.69MB,轻量易下载。代码目录按功能拆分:cutgraph对应割图算法,harmonic_map对应调和映射算法,data内放置测试模型,3rdparty中集成MeshLib网格库与freeglut渲染库,CMakeLists.txt已预置好,在Windows 10、Ubuntu 18.04、macOS 10.15.5上均可编译运行。通过阅读源码,可直观学习网格数据结构的组织、割图路径的构造、调和映射的能量最小化求解流程,以及如何借助Eigen库完成矩阵分解与线性方程组求解,同时代码也覆盖了网格渲染与视角交互的实现思路。目前已有232人学习下载,对正在做相关作业或入门共形几何算法者而言,是一份结构清晰、跨平台适配的可用代码库。 做几何处理这几年,我越来越觉得 Conformal-Geometry(共形几何)是个绕不开的话题。它看起来像是纯数学的名词,但实际上一碰纹理映射、曲面重建、网格参数化,背后全是这套东西。每次有人问我“为什么展开的 UV 还是会扭曲”“为什么保面积效果这么好但不能保角”,我最后都得把话题拉回共形映射的基本性质上。这篇文章就把我对 Conformal-Geometry 的理解、常用的离散化方法、以及实操中踩过的坑整理一遍,给准备做网格处理、三维重建或者参数化方向的朋友一个可参考的路线图。
1. 先想清楚:共形几何到底解决什么问题
1.1 为什么几何处理绕不开“保角”
三维曲面本质上是二维流形,计算机要处理它,最自然的办法就是把它摊平到平面上。但摊平一个曲面几乎总会带来变形:角度会变、面积会变、长度会变。共形几何研究的就是一类特殊的映射,它只保证角度不变,不保证长度和面积不变。这个性质听着简单,用起来却极其关键。
举个例子,你在曲面上画一个十字交叉线,然后把这个区域映射到平面上。无共形约束的映射可能把原来垂直的夹角变成 60 度,共形映射则会严格保持 90 度。很多下游任务对角度非常敏感。比如贴纹理,如果局部角度被改变,贴图上的圆会在曲面上变成椭圆,文字会整体“扭”掉;再比如做有限元分析,网格单元的形状畸变直接影响数值精度,歪斜单元一多,求解器就直接报警。所以共形几何不是理论家在纸面上玩的游戏,它是一切“尽量少破坏形状”的实践的底层支撑。
1.2 一个从球面到平面的生活化例子
理解共形映射,最好的类比是地图投影。地球是球面,地图是平面,把球面摊成平面必然有变形,差别只在变形的优先级。等角投影(比如墨卡托投影)保证地图上任意两条线的夹角和地球上一致,代价是格陵兰岛被放大得离谱;等积投影保证面积关系正确,但角度会歪掉。共形几何做曲面参数化,本质上就是“用一个全局等角投影把曲面展平”,只是映射的源曲面不再是理想球面,而是一个任意拓扑、任意度量的网格。
这个类比还能帮你想清楚一个重要结论:全局不变量和角度不变不可兼得。如果一个曲面整体高斯曲率不为零,你就不可能同时做到既保角又等积,更不可能做到等距。这是 Gauss-Bonnet 定理在背后管着。很多做应用的人一开始不接受“共形映射会放大面积”这个事实,总想额外加一个面积约束,结果角度和面积两头都没守住,纹理质量反而更差。这个矛盾后面在实操部分会反复出现。
2. 共形映射的核心原理:不只是“形状不变”
2.1 保角性的数学含义
形式上,一个映射 (f: U \to V) 称为共形映射,如果它在每一点的微分(雅可比矩阵)是一个相似变换。相似变换就是“旋转 + 均匀缩放”,不包含剪切。说得更直白一点,雅可比矩阵可以写成 (J_f = \lambda R),其中 (R) 是正交矩阵,(\lambda) 是逐点变化的缩放系数。这个写法同时解释了两件事:角度为什么不变——因为正交旋转不改变夹角;面积为什么改变——因为 (\lambda) 在每一点都不同,整体上面积总会发生非均匀的变化。
我当初第一次接触这个定义时,觉得“均匀缩放”是很强的约束,应该很难满足。确实如此。一个任意的曲面到平面的映射,在逐点意义下通常不可能做到雅可比全是相似变换。真正可行的是最小二乘意义下的共形:让全曲面的共形能量尽可能小,而不是严格为零。这样的映射叫“准共形映射”(quasi-conformal map),它有一个非常有用的量化指标叫共形偏差(conformal distortion)。实践中我们追求的不是完美共形,而是把共形偏差压到足够小。
2.2 从复分析到离散网格:拉普拉斯-贝尔特拉米算子
共形映射还有一个很漂亮的复分析解释:如果源曲面是平面上某个区域,那所有共形映射恰好就是全纯函数。全纯函数的本质特征是满足 Cauchy-Riemann 方程,而这个方程用调和函数的话来说就是两个分量都是调和函数。所以“共形”和“调和”之间有天然的通道:先求一个调和映射,再想办法修正它让它满足共形条件,是一条非常经典的技术路线。
到了离散网格上,调和映射对应的就是拉普拉斯-贝尔特拉米算子的离散化。你可以把拉普拉斯矩阵想象成一个测量“局部平均偏离”的算子,求解调和映射的过程,就是在给定边界条件下让某种“全局弯曲能量”最小的过程。整个管道通常长这样:先把三维网格的边界映射到平面上(这一步可以选择圆周、正方形或者自由边界),然后求解一个稀疏线性系统,得到内部顶点的平面坐标。之后如果需要更小的角度扭曲,再用非线性优化对坐标进行迭代修正。后面讲算法对比时你会看到,LSCM 和 ARAP 的差别,本质就在“线性近似”和“非线性迭代修正”这条分界线上。
3. 离散化与实操:把理论落到网格上
3.1 网格参数化的目标函数怎么写
离散共形映射的起点是定义在三角形网格上的目标函数。最直观的做法是对每个三角形定义一个局部形变度量,把所有三角形的形变能量加起来。常用的一个框架是“局部到全局”(Local/Global)交替优化,ARAP(As-Rigid-As-Possible)就是这一类。它的迭代分成两步:局部步骤逐个三角形求最优旋转,全局步骤把所有三角形拼回一个整体坐标。每一步都有闭式解,所以收敛很快。
如果你用的是线性方法,比如 LSCM(Least Squares Conformal Maps),目标函数可以写成一个二次型,求解一次稀疏线性系统就够了。它的做法是把每个三角形上的理想共形条件写成线性方程,然后对所有三角形累积最小二乘能量。线性系统的好处是快、稳定、不需要初值;缺点是它只能逼近共形条件,对于高曲率区域往往会留下明显的角度扭曲。LSCM 对边界不敏感,因为它的能量不要求固定边界,边界顶点可以自由移动,这在实际使用中很适合做艺术化展开。
3.2 常见算法怎么选:LSCM / ARAP / 等距映射
我帮不少朋友做过参数化方案选型,发现大多数人不是不知道算法,而是不知道算法之间的适用边界。这里给一个简洁的对照表,能解决 80% 的选型困惑。
| 算法 | 类型 | 求解方式 | 优点 | 劣势 | 典型场景 |
|---|---|---|---|---|---|
| LSCM | 线性共形 | 稀疏线性系统 | 快,稳定,无需初值 | 高曲率区域扭曲偏大 | 大规模网格、实时交互、UV 展开初版 |
| ARAP | 非线性等距/共形混合 | 局部-全局迭代 | 角度和面积都能控制,质量高 | 慢,需迭代与初值 | 高质量纹理映射、建模交互 |
| 等距映射(Isometric) | 非线性 | 全局优化 | 长度保持最好 | 计算昂贵,容易陷入局部最优 | 扫描配准、布料模拟 |
| 面积保持法 | 非线性 | 约束优化 | 面积扭曲小 | 角度容易崩 | 制图学、统计形状分析 |
我的个人经验是:如果只是给一个中等规模网格做 UV 展开,LSCM 永远是第一个尝试项。它跑得快,能立刻暴露输入网格的质量问题(比如坏边、退化三角形)。问题暴露之后再用 ARAP 做精修,能省下很多调试时间。如果你想从第一步就拿到最好的结果,那直接从 ARAP 开始,但要有心理准备:非线性迭代对初始值有要求,坏输入会让它直接发散。
3.3 一个最小实现流程示意
下面这个流程不是某一个库的官方教程,而是我组合 libigl、CGAL、以及自写后处理脚本做网格参数化时常用的一套最小流程。代码块用 Python 风格伪代码示意,方便移植到自己的工程里。
# 伪代码:网格共形参数化最小流程 import libigl as igl # 1. 输入网格 V(N,3), F(M,3) V, F = igl.read_triangle_mesh("model.obj") # 2. 取边界顶点,映射到圆上(或凸多边形) bnd = igl.boundary_loop(F) bnd_uv = igl.map_vertices_to_circle(V, bnd) # 3. 求解调和映射(线性系统),得到 uv 坐标 uv = igl.harmonic(V, F, bnd, bnd_uv, 1) # 4. 用准共形迭代修正角度扭曲(对应 LSCM/ARAP 修正步骤) for k in range(10): R = igl.local_rotation(V, F, uv) # 局部步骤:求各三角形最优旋转 uv = igl.global_solve(V, F, uv, R) # 全局步骤:重新拼合坐标 # 5. 检查共形能量 / 翻转三角形比例 energy = igl.conformal_energy(V, F, uv) flipped = igl.flipped_triangles(F, uv) print(f"conformal energy: {energy:.4f}, flipped tris: {len(flipped)}")这个流程有一个关键点在第四步:局部-全局迭代中,每一轮都在“保持局部角度关系”和“拼回全局坐标”之间交替。收敛之后你会看到 UV 布整体趋于等距,但不会完全等距,因为三角形本身与平面之间天然存在内蕴形变,这是任何映射都消除不了的。
4. 落地过程中最容易踩的坑
4.1 翻转三角形和面积退化
我最早做参数化时,最容易遇到的问题就是翻转三角形。现象是 UV 平面上出现一组法线方向反转的三角形,渲染结果表现为纹理处黑斑或者面片重叠。出现翻转的直接原因大多是初始映射质量太差,或者网格本身包含狭长三角形。处理办法有三层:第一层是在输入阶段做网格清理,把长宽比过大的三角形、重复顶点、退化边全部处理掉;第二层是在求解后检测翻转,按“面积变号”判断翻转三角形数量;第三层是在优化能量里加入翻转惩罚项,让迭代主动避开翻转区域。
一个值得记住的检查点是:LSCM 得到的 UV 即使没有翻转,也可能出现面积退化——某个区域被压得特别小,纹理细节全挤在一起。这时候不要只盯着共形能量看,还要统计 UV 三角形面积和原始网格三角形面积的比值分布。如果最大最小比值超过 50 倍,基本就是输入网格的局部曲率过高或者边界约束不合理。单纯提高分辨率并不能解决这个问题,根源在于共形映射本身就只能做到“角度最优”,没义务保证面积均匀。
4.2 边界固定导致的严重扭曲
很多人做 UV 展开时习惯把边界固定到一个正方形或者圆形上,认为这样 UV 布局更“规整”,方便后续排布。这个做法在低曲率网格上问题不大,遇到高弯曲的网格(比如手背、耳朵这类区域)就会出大问题。因为边界位置本身就是对内部点的一个强约束,如果边界被硬按到预定义的形状上,内部点只能通过极端拉伸来补偿,结果共形能量飙升。
我的建议是:第一遍求解不要固定边界,让边界顶点自由落在平面上,这样得到的共形映射通常扭曲最小;第二遍再考虑要不要对边界做规整化处理。如果你确实需要规整的方形边界,比如做地形渲染或者体素化纹理,那就在自由优化结果的基础上做一次带边框约束的全局平滑,而不是从一开始就强加边界。实践下来,这种“两步走”能让最终扭曲比一次性固定边界小一个量级。
4.3 一个容易被忽略的错误直觉:共形不等于不拉伸
最后这个坑既是技术问题,也是认知问题。共形映射只保证角度,不保证长度和面积。这意味着一个圆经过共形映射后还是“局部相似”的——注意是局部,不是全局。高曲率的区域在共形映射中经常被放大,面积跟着膨胀,纹理贴上去之后局部分辨率不统一。如果你做的是工业级纹理烘焙,一定要在纹理分辨率的分配阶段,用面积比作为权重去调整不同区域的纹素密度。
我见过一个实际案例:一个雕刻精细的手办模型,用共形映射展开之后,脸部细节贴图看起来糊成一片,背部却什么都没有。问题不在贴图画得不好,而在 UV 展开时脸部区域的面积被压缩了太多,导致同样像素数的纹理在脸部比在背部密得多。解决办法其实不难:用面积扭曲可视化工具(按三角形面积比上色)直接能看到哪些区域被压扁了,再在那些区域增加纹理分辨率,或者改用混合参数化方法给敏感区域预留更多面积。
这里把常见的坑整理成一张速查表,方便你排查问题时直接对照:
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 渲染出现黑斑、纹理错乱 | UV 存在翻转三角形 | 统计翻转三角形数量 | 网格清理、加翻转惩罚 |
| 局部纹理细节糊成一团 | 面积退化 | 面积比分布可视化 | 局部加密纹理、改混合参数化 |
| 边界固定后能量飙升 | 边界约束过强 | 对比自由边界与固定边界能量 | 先自由优化再规整边界 |
| 迭代发散 | 初始值太差或网格坏元素 | 检查是否有负面积/退化三角形 | 换线性初值、先净化网格 |
| 结果整体偏斜 | 离散化精度不够 | 加密网格后对比能量 | 网格重网格化后再参数化 |
5. 再往前走一步:从参数化到更多应用
5.1 纹理映射与烘焙的落地细节
纹理映射是共形几何在图形学里最直接的落脚点。URP 或 HDRP 管线下,无论是贴花、地形混合还是次表面贴图,都需要一套干净的 UV。共形参数化做出的 UV 有个好处:由于角度保持很好,纹理接缝处的法线方向连续性强,烘焙出来的 normal map 接缝不容易出现硬边。
做 UV 展开之前,我强烈建议先设定好接缝(seam)。接缝的位置直接影响参数化的能量分布,接缝选不好,后续展开质量怎么优化都有限。我的经验是:把接缝藏在高曲率、低视觉注意力的区域,比如头部的耳后、颈部后侧、雕像底部。选完接缝之后,把网格沿接缝切开,变成一个或多个拓扑圆盘,再用 LSCM 生成 UV。这样处理的纹理能保持高一致性,出问题的概率最小。
5.2 从几何处理到形状分析:共形表示的长尾价值
共形映射不止在纹理映射里有用,也是三维形状分析的重要工具。医学影像里的脑皮层重建就是一个典型场景:把高度褶皱的脑皮层曲面映射到一个圆盘或者球面上,然后在这个规范域上做特征对齐、统计建模。因为共形映射保持角度,在规范域上标记的特征点可以可靠地对应回原始曲面,不会因为角度变化导致特征点相对位置漂移。
曲面配准和变形传输也大量使用准共形映射。给定两个拓扑等价的三维模型,先各自映射到规范域,再在规范域上做形变,就能把形变从一个模型传递到另一个。这种方法在姿态迁移、表情迁移中有非常成熟的应用。我自己做过一个实验:把一张高精度三维扫描的脸部形变,通过共形参数化传到另一个粗糙模型上,效果比直接在三维空间做最近邻传输稳定得多,而且不会在五官区域产生明显滑移。
5.3 基于实际项目的一点体会
做 Conformal-Geometry 方向的项目,我发现最核心的能力不是推导数学公式,而是能判断“这条曲线能不能被保角”,以及“扭曲大了到底该怪映射还是怪输入网格”。很多几何处理的报错,追根溯源都是输入网格质量太差,或者对“共形”的期望值过高。先用 LSCM 快速验证拓扑是否正确,再用 ARAP 精修质量,最后用面积扭曲可视化来检查纹理分配是否合理——这三步组合能覆盖我目前遇到的大部分任务。
最后再分享一个操控细节:无论用哪个库,都不要忽略浮点精度对边界计算的影响。网格文件里常见的重复顶点、非流形边,会让边界环检测直接返回错误结果,后续所有计算全白做。所以在输入环节多花五分钟做网格清理,比在输出端花五小时调参数更划算。
本文还有配套的精品资源,点击获取