
三维模型从扫描仪里出来网格干干净净拓扑也清理过结果一进游戏引擎做纹理绘制角度全被拉伸成一条条斜纹高光贴图也完全对不上。这种情况我遇到过太多次了最后基本都是同一个解法——LSCMLeast Squares Conformal Maps最小二乘保角参数化。这项技术从2002年Levy等人提出到现在二十多年过去依然是纹理映射、法线贴图烘焙和重网格化里绕不开的基础工具。这篇文章我会把这套方法的数学原理、代码实现和实战排错整个过一遍适合正在做全局参数化相关项目、对UV展开质量不满意的开发者或者准备入门几何处理的研究生。读完你不但能自己从零实现一个LSCM求解器还能知道遇到翻转、数值不稳定时该怎么处理。1. 参数化在解决什么问题为什么角度畸变最让人头疼1.1 三维曲面熨平到二维的本质参数化的目标非常直白给三维网格曲面上的每个顶点找到一组二维坐标使得从三维到二维的映射在某种度量下失真最小。这相当于把一张橘子皮压成一片橘子皮干无论你怎么压总会有些地方皱起来有些地方被撑开。数学上曲面到平面的映射必然存在畸变除非这个曲面本身是可展曲面比如圆柱侧面能被无扭曲地铺平。畸变按照性质分成三类角度畸变、面积畸变和长度畸变。角度畸变指原本垂直的两条切线方向在参数化后不再是直角体现到视觉上就是纹理图案被斜向拉伸圆点变成椭圆棋盘格变成平行四边形。面积畸变指参数域上的单位面积对应曲面上的面积不均匀同一个纹理元素在模型不同部位被放大或缩小。长度畸变则是沿不同方向的尺度变化不一致是角度和面积畸变的共同来源。对于纹理映射这个最常见的应用场景来说角度畸变是影响最大的。因为人的视觉系统对纹理图案的方向性非常敏感只要角度偏了几度整个模型看起来就像被揉过。而面积畸变虽然会改变纹理密度但可以通过后续的纹理密度调整来缓解。LSCM正是冲着角度畸变去的它要让映射在局部尽量保持各向同性也就是只做旋转和均匀缩放不做剪切变形。1.2 保角映射的独特地位保角映射Conformal Map也叫共形映射是复分析里最核心的概念之一。它有一个非常好的性质局部上映射只包含旋转和缩放不包含剪切。用矩阵的语言说映射的雅可比矩阵每一点都可以写成某个标量乘以一个正交矩阵。这意味着参数化前后小区域的形状保持相似只是大小可能变化。参数化算法里不保证保角的方案大多会引入明显的剪切变形而过于强调保面积的方案往往会在角度上崩掉。LSCM取了一个很好的平衡它通过最小二乘的方式逼近共形条件既能保持较高质量的角度保真又只需要解一个线性方程组不需要像非线性优化那样迭代收敛。这也解释了为什么LSCM在工业界和学术界都长盛不衰——它是少数能在性能和效果之间取得良好平衡的方案。1.3 为什么二十年了还在用LSCM有人可能会问现在ARAP、ABF这些更高级的方法都出来了为什么还抱着LSCM不放很简单LSCM是线性的求解一个稀疏线性系统在几百万面片的模型上也就几秒钟的事情而ARAP这类非线性方法要迭代几十轮一次迭代就要解一个线性系统。在纹理映射任务中LSCM的角度畸变控制通常已经足够好剩下的那点误差人眼很难察觉。尤其在做UV展开时边界是自由边界LSCM能在不人为切割的情况下得到很自然的展开结果这比基于固定边界的Tutte嵌入好得多。2. 共形条件在网格上怎么表达从连续到离散2.1 连续共形映射的Cauchy-Riemann判据在连续情形下设曲面上某点的局部坐标为(x, y)参数化映射写成复函数形式f u iv其中u和v就是该点在参数平面上的两个坐标分量。这个映射共形的充分必要条件是满足Cauchy-Riemann方程∂u/∂x ∂v/∂y ∂u/∂y -∂v/∂x从几何上看这两条方程合在一起的含义是在任意微小区域内映射的雅可比矩阵形如[[a, -b], [b, a]]也就是旋转加上均匀缩放。用复分析的术语说共形映射就是全纯函数满足∂f/∂z̄ 0。这里的z x iy微分算子∂/∂z̄ (∂/∂x i∂/∂y)/2 衡量的是映射对z的共轭部分的依赖程度等于零说明映射不依赖z̄纯保有z的信息。2.2 三角形网格上的离散化策略网格不是光滑曲面但我们可以把它看成由三角形组成的分片线性曲面。在每个三角形内部参数化坐标(u, v)通过顶点上的值做线性插值所以梯度在每个三角形上是常数。这样Cauchy-Riemann方程就被离散到每个三角形上要求每个三角形内部的梯度满足共形条件。具体怎么做对三角形T三个顶点在三维空间中的坐标为p1、p2、p3在参数平面上的坐标为(u1, v1)、(u2, v2)、(u3, v3)。三角形内u和v的梯度可以由线性插值基函数的梯度叠加得到。设φ1、φ2、φ3是重心坐标基函数则∂u/∂x Σ ui * ∂φi/∂x ∂u/∂y Σ ui * ∂φi/∂y ∂v/∂x Σ vi * ∂φi/∂x ∂v/∂y Σ vi * ∂φi/∂y而基函数的梯度只和三角形的几何形状有关在二维局部坐标系下可以写成显式公式。这样一来共形条件就被转化为一组关于未知量(ui, vi)的线性方程。2.3 精确求解不可行退而求最小二乘这里有个关键问题每个三角形给出一组共形约束一共两个方程整个网格有F个三角形、V个顶点。因为每个三角形贡献两个方程而每个顶点贡献两个未知量(ui, vi)方程总数是2F未知量总数是2V。对于一个三角网格欧拉公式给出F ≈ 2V的关系所以方程数量大约是未知量数量的两倍——这是一个过定系统所有三角形同时严格满足共形条件在一个非可展曲面上根本不可能。Levy等人的核心想法是既然不能严格满足就把它变成最小二乘问题。对每个三角形定义残差r1 ∂u/∂x - ∂v/∂y r2 ∂u/∂y ∂v/∂x然后最小化所有三角形残差平方的面积加权和E_LSCM Σ_T A_T * (r1² r2²)这里的A_T是三角形的面积权重为什么要加权因为面积大的三角形代表更大的曲面区域在能量里占更高的权重是合理的。如果不加权优化会倾向于把误差集中到面积很大的三角形上而不管它们导致大面积区域畸变严重。加上面积权重后能量最小化才真正反映了整体畸变的平均程度。把残差展开能量E是参数坐标的二次型所以对它的最小化等价于求解一个线性方程组。这正是LSCM最迷人的地方——一个表面上看起来需要非线性优化的几何问题通过巧妙的线性化最终变成了一个随时可以求解的稀疏最小二乘问题。3. 从零实现LSCM的关键计算细节3.1 局部坐标系与面积计算实现LSCM第一步是处理几何数据。每个三角形在三维空间中有一个法向量我们需要在三角形所在的切平面上建立一套二维局部坐标这样梯度计算才有意义。操作流程如下计算三角形法向量n normalize((p2 - p1) × (p3 - p1))以p1为原点令e1 normalize(p2 - p1)令e2 n × e1得到切平面上的正交基将p2、p3投影到(e1, e2)张成的平面上x1 0, y1 0x2 |p2 - p1|, y2 0x3 (p3 - p1)·e1, y3 (p3 - p1)·e2三角形面积A |x2y3 - x3y2| / 2这样建立的局部坐标系保证了x1 y1 y2 0接下来梯度公式会简化很多。但用完整公式写代码的时候保持通用形式更稳妥不依赖坐标原点位置。3.2 梯度算子与LSCM矩阵组装三角形内线性插值的梯度可以通过标准公式计算。设三角形的三个顶点在局部坐标下为(x1, y1)、(x2, y2)、(x3, y3)面积为A那么三个基函数的梯度是∇φ1 ((y2 - y3)/(2A), (x3 - x2)/(2A)) ∇φ2 ((y3 - y1)/(2A), (x1 - x3)/(2A)) ∇φ3 ((y1 - y2)/(2A), (x2 - x1)/(2A))这个公式很直观每个基函数φi在顶点i处为1在其对边处线性衰减到0所以梯度指向对边方向大小和对边长度成正比除以2A做归一化。有了梯度基函数每个三角形残差r1、r2可以写成关于6个未知量(u1, v1, u2, v2, u3, v3)的线性组合r1 Σ ui * ∂φi/∂x - Σ vi * ∂φi/∂y r2 Σ ui * ∂φi/∂y Σ vi * ∂φi/∂x注意每个三角形有两行残差最终组装的LSCM矩阵M是一个2F×2V的稀疏矩阵F是三角形数量V是顶点数量。每行包含6个非零元素对应三角形三个顶点各自的两个坐标分量。加面积权重时直接把这一行的所有系数乘以sqrt(A_T)。3.3 秩亏问题与固定顶点约束LSCM的矩阵M有严重的秩亏问题。从连续情形看任意一个共形映射f如果满足∂f/∂z̄ 0它都落在M的零空间里。对于平面域全纯函数的空间是无限维的离散网格上秩亏具体表现为如果对网格上的所有点都加上同样的二维平移参数化结果不变对所有点做全局旋转、均匀缩放LSCM能量完全不变。这意味着线性系统有一个至少四维的零空间二维平移、一维旋转、一维缩放有时还更多取决于网格拓扑。为了消除这些自由度必须固定一些顶点的参数坐标。最少固定两个顶点每个顶点提供两个约束正好消掉四个自由度。实际操作中常见做法是固定两个相距较远的顶点比如Bounding Box对角线两端的顶点。固定时设置它们的UV坐标比如固定为(0, 0)和(1, 0)或者(0, 0)和(0.5, 0)。处理约束的方式有两种常用思路。第一种是把固定顶点的列从矩阵中移出作为右端常数项再对自由变量求解M_free^T M_free x_free -M_free^T M_fixed x_fixed第二种是用罚函数直接往正规方程的对角线加一个大数λ比如10^8强迫解落在固定值上。罚函数实现更简单但会稍微污染条件数网格顶点多的时候建议用第一种。3.4 求解器选择与复杂度求解除稀疏线性系统外没有别的技巧。推荐流程是先显式构造正规方程A M_free^T M_free然后用Eigen的SimplicialLDLT或SparseQR求解。SparseQR更稳定SimplicialLDLT更快。在实际工程中如果三角形个数达到百万规模正规方程的稀疏Cholesky分解通常能在几秒到十几秒内完成。需要注意正规方程会平方矩阵的条件数所以网格质量太差时数值上容易出问题建议对M做一次平衡或者直接对稀疏矩形矩阵M_free用SparseQR求解最小二乘虽然慢一点但稳定。4. 基于Eigen的求解代码照着写就能跑4.1 数据结构和预处理代码实现时需要一个简单的网格数据结构包含顶点坐标和三角形索引struct Mesh { std::vectorEigen::Vector3d V; // 顶点坐标 std::vectorEigen::Vector3i F; // 三角形顶点索引 (i0, i1, i2) int Vn V.size(); int Fn F.size(); };预处理阶段遍历每个三角形计算局部坐标、梯度和面积。为了组装稀疏矩阵先把每行的非零元素存在一个Coordinate List里全部构造完再压缩成稀疏矩阵std::vectorEigen::Tripletdouble triplets; for (int t 0; t Fn; t) { const auto tri F[t]; const Eigen::Vector3d p1 V[tri[0]]; const Eigen::Vector3d p2 V[tri[1]]; const Eigen::Vector3d p3 V[tri[2]]; // 局部坐标系 Eigen::Vector3d n (p2 - p1).cross(p3 - p1); double A n.norm() / 2.0; if (A 1e-12) continue; // 退化三角形直接跳过 n.normalize(); Eigen::Vector3d e1 (p2 - p1).normalized(); Eigen::Vector3d e2 n.cross(e1); // 局部二维坐标 double x2 (p2 - p1).norm(); double x3 (p3 - p1).dot(e1); double y3 (p3 - p1).dot(e2); // 基函数梯度 double inv_2A 1.0 / (2.0 * A); double dphi1_dx (0 - y3) * inv_2A; double dphi1_dy (x3 - x2) * inv_2A; double dphi2_dx (y3 - 0) * inv_2A; double dphi2_dy (0 - x3) * inv_2A; double dphi3_dx (0 - 0) * inv_2A; double dphi3_dy (x2 - 0) * inv_2A; // 常数因子 sqrt(A) 已在组装时乘入 }4.2 LSCM矩阵组装确定每个顶点的全局未知量索引顶点i的u坐标索引为2iv坐标索引为2i 1。对每个三角形t它在LSCM矩阵M中占据第2t行和第2t1行double sqrtA std::sqrt(A); int row1 2 * t; int row2 2 * t 1; // 系数矩阵每个变量前面的系数 // 行1对应 r1 ∂u/∂x - ∂v/∂y triplets.emplace_back(row1, 2*id0, dphi1_dx * sqrtA); triplets.emplace_back(row1, 2*id1, dphi2_dx * sqrtA); triplets.emplace_back(row1, 2*id2, dphi3_dx * sqrtA); triplets.emplace_back(row1, 2*id01, -dphi1_dy * sqrtA); triplets.emplace_back(row1, 2*id11, -dphi2_dy * sqrtA); triplets.emplace_back(row1, 2*id21, -dphi3_dy * sqrtA); // 行2对应 r2 ∂u/∂y ∂v/∂x triplets.emplace_back(row2, 2*id0, dphi1_dy * sqrtA); triplets.emplace_back(row2, 2*id1, dphi2_dy * sqrtA); triplets.emplace_back(row2, 2*id2, dphi3_dy * sqrtA); triplets.emplace_back(row2, 2*id01, dphi1_dx * sqrtA); triplets.emplace_back(row2, 2*id11, dphi2_dx * sqrtA); triplets.emplace_back(row2, 2*id21, dphi3_dx * sqrtA); // 目标值都是0不需额外的b向量最后压缩成稀疏矩阵Eigen::SparseMatrixdouble M(2 * Fn, 2 * Vn); M.setFromTriplets(triplets.begin(), triplets.end());4.3 固定约束与求解固定两个顶点b0和b1设定它们的UV坐标bc0和bc1。重排矩阵时先标记自由变量和固定变量然后把固定变量的列拆出去// 构建自由变量的索引映射 std::vectorint free_cols, fixed_cols; std::vectordouble fixed_vals; for (int i 0; i Vn; i) { if (i b0 || i b1) { fixed_cols.push_back(2*i); fixed_cols.push_back(2*i1); fixed_vals.push_back(i b0 ? bc0.x() : bc1.x()); fixed_vals.push_back(i b0 ? bc0.y() : bc1.y()); } else { free_cols.push_back(2*i); free_cols.push_back(2*i1); } } Eigen::SparseMatrixdouble M_free(2*Fn, free_cols.size()); Eigen::SparseMatrixdouble M_fixed(2*Fn, fixed_cols.size()); // 逐列从M中抽取构造两个矩阵 Eigen::SparseMatrixdouble A M_free.transpose() * M_free; Eigen::VectorXd b -(M_free.transpose() * (M_fixed * Eigen::VectorXd(fixed_vals))); Eigen::SimplicialLDLTEigen::SparseMatrixdouble solver; solver.compute(A); Eigen::VectorXd x solver.solve(b); // 输出结果 Eigen::MatrixXd V_uv(Vn, 2); for (size_t k 0; k free_cols.size(); k) { int idx free_cols[k]; V_uv(idx/2, idx%2) x[k]; } for (size_t k 0; k fixed_cols.size(); k) { int idx fixed_cols[k]; V_uv(idx/2, idx%2) fixed_vals[k]; }4.4 结果检查与畸变量化跑完求解器不能直接看要先做简单检查。最直接的是检查有没有发生三角形翻转判断每个三角形在参数平面上的有向面积符号如果有三角形面积为负说明它在参数化时翻面了。LSCM对凹边界或高曲率区域的三角形翻转比较常见。角度畸变的量化可以用每个三角形的共形能量残差来判断double total_energy 0.0; for (int t 0; t Fn; t) { // 用参数坐标计算实际梯度代入 r1, r2 // 累加 A_T * (r1^2 r2^2) }这个能量值在LSCM里没有绝对标准不同模型、不同面积分布差异很大但它可以用来对比同一模型在不同固定点、不同预处理下的结果好坏。对数直方图或伪彩色映射到网格表面能直观看到哪些区域畸变集中。5. 实操踩坑记录翻车现场与修复方案5.1 三角形翻转LSCM不保证全局无翻转LSCM本质上是一个基于梯度的能量它惩罚的是梯度之间的偏差而不是映射的局部朝向问题。如果模型包含凹陷区域或者边界形状非凸参数化后的三角形很可能整体或者局部翻过去。翻转的典型表现是UV上出现了重叠的负面积三角形纹理在这些区域出现镜像效果。排查时先定位翻转三角形看它们集中在哪个区域。如果是高曲率的凸起或凹窝附近这是LSCM能量本身的倾向导致的。最简单的修复策略是把LSCM的结果作为初始值再用无翻转约束的后处理算法比如Tutte嵌入后处理、BVH优化或者诱导式的重新投影修正。如果项目对实时性要求高可以尝试在LSCM能量后面额外加一个小的面积正则项但面积正则会牺牲角度质量需要调参。5.2 固定顶点位置对结果的影响固定顶点的选取对LSCM结果的影响超出很多人的预期。固定两个距离很近的顶点整个参数化会产生巨大的缩放拉伸因为你强制了两个相邻点之间的距离而LSCM的能量又把其他区域按比例缩放最终可能导致大量三角形被压扁。固定点一定要选在Bounding Box的对角线两端相距尽可能远。固定值本身倒不用太讲究通常选(0,0)和(1,0)就能用。如果你想把参数域放到某个特定区域可以先定(0,0)和(s,0)跑完后用最小二乘拟合把结果整体旋转平移到目标区域不要在求解时强行约束。5.3 网格质量对数值稳定性的影响LSCM对三角形质量很敏感。网格里只要混入几个面积趋近于零的退化三角形梯度基函数公式里的除以2A就会把它们变成巨大的系数这些行在最小二乘里占据压倒性权重导致求解出来的参数化被这几个坏三角形主导。网格整体看起来正常但某些区域出现波浪形扭曲。解决办法分两步预处理时直接跳过面积小于某个阈值的三角形比如平均面积的百万分之一或者更稳妥地在计算梯度时对过小的面积做clamp操作。正规方程的数值条件数也会因为退化三角形恶化表现为求解结果出现NaN或者极端值。加上SparseQR替代SimplicialLDLT通常能缓解。5.4 面积权重的正反两面面积加权是LSCM的标准配置但它也有副作用。如果网格表面存在极不均匀的三角形密度——比如扫描模型在精细区域有极细的三角形在平坦区域有极大的三角形——面积权重会让大三角形区域的误差控制得特别好而小三角形区域被牺牲。这在重网格化和细分任务中会造成局部细节畸变。如果遇到这种情况试试把面积权重换成空权重即所有权重为1或者用基于边长倒数的权重。换句话说对特定应用场景LSCM的鲁棒性很大程度上来自权重的选择值得针对你的数据反复实验。6. 与Tutte嵌入、ARAP、ABF的横向对比及选型建议6.1 Tutte嵌入保拓扑不保角Tutte嵌入的核心思路是把边界顶点固定到一个凸多边形上然后每个内部顶点取为其邻居顶点的重心坐标。它保证生成的参数化一定是无翻转的这是最大的优点。但代价是角度畸变控制很弱尤其当网格内部有严重弯曲时Tutte嵌入会产生明显的面积拉长和角度剪切。Tutte嵌入真正的舞台是作为更复杂算法的初始化。LSCM是线性的不需要初始化也能直接解但如果你的LSCM结果翻转严重可以先跑一遍Tutte嵌入把结果作为LSCM的软约束或者作为ARAP类算法的起点。6.2 ARAP更强的局部刚性但代价高ARAPAs-Rigid-As-Possible参数化在LSCM的能量基础上额外要求每个三角形的变换尽量接近刚体变换只旋转不平移更不允许剪切。它的角度质量比LSCM好不少尤其是复杂模型的纹理映射效果肉眼可见地更自然。问题是ARAP是非线性迭代算法每轮迭代都要重新计算局部最优旋转并求解全局线性系统收敛需要几十轮甚至上百轮在百万面片网格上有点熬人。选型建议很直接如果模型量级小几千到几万面片且对质量要求高直接用ARAP如果是百万级的大模型只是做快速UV预览LSCM更合适。6.3 ABF角度畸变的天花板ABFAngle-Based Flattening是一个很有意思的思路它直接以三角形的内角为优化变量让所有内角变量加和满足三角形约束和顶点加和约束然后最小化内角与原始角度的偏差。这种方法在角度保真方面是公认的标杆几乎找不到明显角度畸变。但它的三角形边长不能直接控制面积畸变可以非常夸张展平后的UV区域甚至会出现大面积重叠。ABF在角点敏感的场景比如展UV做角色脸部贴图里很出色但做自动化批量处理时LSCM的稳定性和可控性更占优势。6.4 场景化选型小结方法角度保真面积控制无翻转保证求解性质典型延迟Tutte嵌入弱弱是凸边界线性毫秒级LSCM中-良中否线性秒级ARAP良-优良有限非线性迭代分钟级ABF优秀差否线性-迭代秒-分钟级如果只是给角色做游戏贴图LSCM配上一两次后处理修翻转是最划算的做精确的几何算法研究、希望参数化不引入额外的形状偏差ARAP是默认选择其他工具需要初始化时用Tutte嵌入热机。7. 扩展思路LSCM不只是贴图工具LSCM的结果本质上是网格在复平面上的一个共形坐标这个坐标信息在不少领域都有额外用途。一个常见的方向是做曲面间的对应粗糙模型和精细模型的LSCM参数化若各自定义在同一个平面域上就能通过平面坐标的最近邻查找建立顶点对应关系这是纹理迁移、形状插值和属性传递的廉价加速方案。另一个值得试的方向是把LSCM和对称检测结合。对于近似对称的人脸或者工业零件模型LSCM展开后的UV坐标在对称轴上应该自然对齐直接做镜像检测就能辅助自动补UV接缝。还有人把LSCM的线性系统拆出来用增量求解实现交互式编辑——用户拖拽一个顶点立刻重新计算整体参数化这在雕刻软件的展UV模块里已经是很成熟的应用。从我自己的实践来看LSCM虽然看起来简单但工程质量的关键往往不在核心公式而在权重调节、固定点策略和退化三角形处理这些细节上。建议拿到一个新模型时先做个快速实验固定Bounding Box对角顶点跑一遍LSCM把能量残差按三角形伪彩色显示一眼就能判断这个模型适不适合直接用LSCM还是需要先切割、先做网格修复、再上参数化。这套流程走顺了LSCM绝对是工具箱里最高性价比的那个工具。