1. 从两个方向理解几何处理细分与简化几何处理里有一对方向完全相反、但经常被放在一起讨论的操作曲面细分和曲面简化。前者是把一个粗糙的低模变成光滑的高模后者是把一个高模压成轻量的低模。它们解决的是同一个核心矛盾——模型精度和计算开销之间的平衡。我做三维相关项目这些年踩过最多的坑基本都集中在这两个环节。细分做过头面数爆炸渲染直接卡死简化做太狠模型棱角全丢法线还乱掉。所以这篇就把这两个操作从原理到实操完整拆一遍包括算法选型、参数计算、代码实现和排查经验。不管你是刚接触图形学课程的学生还是需要在实际项目里处理网格的开发者应该都能找到能直接用的东西。先明确一下这两个概念到底在干什么。一个三角网格由顶点、边、面三部分组成细分就是在现有网格基础上按规则插入新顶点、重新连接拓扑让表面更细腻简化则是反过来合并顶点、塌缩边用更少的面去逼近原来的形状。两者共享同一套网格数据结构半边结构、邻接表之类所以放在一起学效率最高。2. 曲面细分原理、算法与实操2.1 为什么需要细分它到底解决什么问题最直接的场景你从建模软件里导出一个低面数的模型直接渲染出来是一块一块的平面光照上去有明显的棱。这时候有两种做法一种是直接加高模但高模文件大、传输慢、显存吃紧另一种就是保留低模在渲染管线里做细分运行时动态生成光滑表面。细分还有第二个用途让网格拓扑更规整。很多扫描或者程序生成的网格三角形大小不一、形状畸形直接做后续处理比如形变、碰撞会出问题。细分可以把这些不规则区域重新划分得到更均匀的三角形分布。第三个用途是配合位移贴图。低模细分之后顶点密度上来了再叠加位移贴图就能在不增加美术工作量的前提下做出表面细节。这个在游戏和影视里非常常见。所以细分不是单纯地加面它是一套可控的、有数学保证的网格加密手段。理解这一点后面的算法选择才有依据。2.2 细分算法的两大流派逼近型与插值型细分算法按新顶点是否保留在原曲面上分成两大类。逼近型Approximating新生成的极限曲面不经过原始顶点原始顶点会被拉向内部。典型代表是Catmull-Clark算法。它的好处是曲面天然光滑C2连续除了奇异点附近缺点是原始形状会有轻微收缩如果你要求模型必须精确经过某些控制点就不合适。插值型Interpolating极限曲面精确经过所有原始顶点。典型代表是Loop算法针对三角网格和Butterfly算法。好处是形状保真缺点是曲面光滑度略差而且容易出现鼓包或者自交。选哪个取决于你的需求。做角色模型、需要光滑过渡的用 Catmull-Clark做地形、需要精确控制点的用插值型。我个人的经验是大部分游戏和影视场景用 Catmull-Clark 就够了因为视觉上更讨喜收缩问题可以通过预先补偿解决。2.3 Catmull-Clark 细分一步步拆开看Catmull-Clark 是最经典的细分方案虽然它最初是为四边形网格设计的但对任意多边形网格都能处理。它的每一轮细分分三步走。第一步生成面点Face Point。对每个面取它所有顶点的平均位置作为这个面的新顶点。F (V1 V2 ... Vn) / n第二步生成边点Edge Point。对每条边取它两个端点和相邻两个面点的平均。E (V1 V2 F1 F2) / 4如果是边界边只有一个相邻面则简化为两端点的平均E (V1 V2) / 2第三步更新原始顶点Vertex Point。对每个原始顶点根据它的价数 n连接的边数计算新位置。V (F_avg 2 * R_avg (n - 3) * V) / n其中 F_avg 是相邻面点的平均R_avg 是相邻边中点的平均。这个公式是 Catmull-Clark 的核心它保证了极限曲面的光滑性。一轮细分之后每个 n 边形面会变成 n 个四边形面。所以如果你从三角形网格开始第一轮之后全变成四边形之后每轮面数乘以 4。注意Catmull-Clark 对三角形网格也能用但第一轮之后拓扑会变成混合四边形如果你后续依赖纯三角网格需要额外做三角化。2.4 Loop 细分三角网格的专用方案如果你的网格全是三角形Loop 细分更合适。它由 Charles Loop 在 1987 年提出同样是逼近型但专门针对三角网格。Loop 细分的规则边点对内部边取两端点和两个对面顶点的加权平均。E (3/8) * (V1 V2) (1/8) * (V3 V4)其中 V3、V4 是共享这条边的两个三角形的对面顶点。顶点更新对价为 n 的顶点用下面的权重更新。V (1 - n * beta) * V beta * sum(neighbors)beta 的取值有个经典公式beta (1/n) * (5/8 - (3/8 (1/4) * cos(2*pi/n))^2)n3 时 beta 约等于 0.1875n6 时 beta 约等于 0.0625。n6 是规则顶点细分后形状保持得最好。Loop 细分每轮把每个三角形变成 4 个小三角形面数乘以 4。它的极限曲面在规则区域是 C2 连续在奇异点附近是 C1。2.5 实操用 Python 实现一轮 Catmull-Clark光看公式容易晕直接上代码。下面是一个简化版的一轮 Catmull-Clark 实现用半边结构的思想组织数据但为了可读性用了字典。import numpy as np def catmull_clark_once(vertices, faces): vertices: (N, 3) 顶点坐标 faces: list of list每个面是顶点索引列表 返回细分后的 vertices, faces vertices np.array(vertices, dtypefloat) n_verts len(vertices) # 1. 计算面点 face_points [] for f in faces: face_points.append(vertices[f].mean(axis0)) face_points np.array(face_points) # 2. 建立边到相邻面的映射 edge_faces {} for fi, f in enumerate(faces): n len(f) for i in range(n): v1, v2 f[i], f[(i 1) % n] key (min(v1, v2), max(v1, v2)) edge_faces.setdefault(key, []).append(fi) # 3. 计算边点 edge_points {} for (v1, v2), adj_faces in edge_faces.items(): if len(adj_faces) 2: f1, f2 adj_faces p (vertices[v1] vertices[v2] face_points[f1] face_points[f2]) / 4.0 else: p (vertices[v1] vertices[v2]) / 2.0 edge_points[(v1, v2)] p # 4. 更新原始顶点 # 统计每个顶点的相邻面、相邻边中点 vert_faces {i: [] for i in range(n_verts)} vert_edges {i: [] for i in range(n_verts)} for fi, f in enumerate(faces): for v in f: vert_faces[v].append(fi) for (v1, v2) in edge_faces: vert_edges[v1].append((v1, v2)) vert_edges[v2].append((v1, v2)) new_orig np.zeros_like(vertices) for v in range(n_verts): n len(vert_edges[v]) if n 0: new_orig[v] vertices[v] continue F np.mean([face_points[fi] for fi in vert_faces[v]], axis0) R np.mean([(vertices[e[0]] vertices[e[1]]) / 2.0 for e in vert_edges[v]], axis0) new_orig[v] (F 2 * R (n - 3) * vertices[v]) / n # 5. 组装新网格 new_vertices [] # 先放更新后的原始顶点 for v in range(n_verts): new_vertices.append(new_orig[v]) # 再放边点 edge_index {} for key in edge_points: edge_index[key] len(new_vertices) new_vertices.append(edge_points[key]) # 最后放面点 face_index {} for fi in range(len(faces)): face_index[fi] len(new_vertices) new_vertices.append(face_points[fi]) # 6. 重新连接每个 n 边形面变成 n 个四边形 new_faces [] for fi, f in enumerate(faces): n len(f) for i in range(n): v_cur f[i] v_next f[(i 1) % n] v_prev f[(i - 1) % n] e_next edge_index[(min(v_cur, v_next), max(v_cur, v_next))] e_prev edge_index[(min(v_prev, v_cur), max(v_prev, v_cur))] new_faces.append([v_cur, e_next, face_index[fi], e_prev]) return np.array(new_vertices), new_faces这段代码可以直接跑输入一个立方体的 8 个顶点和 6 个四边形面跑一轮就能看到形状开始变圆。跑三轮以上基本就是一个光滑的球状体了。实操心得第一轮细分之后所有面都变成四边形。如果你需要继续细分直接递归调用即可。但要注意面数是指数增长的立方体 6 个面一轮变 24 个两轮 96 个三轮 384 个四轮 1536 个。实际项目里通常控制在 2 到 3 轮。2.6 细分中的边界处理与常见坑边界边和边界顶点是细分里最容易出问题的地方。上面代码里对边界边用了简单的平均但边界顶点的更新规则需要单独处理否则边界会收缩或者扭曲。标准做法是边界顶点只根据它相邻的两条边界边来更新用类似曲线细分比如 Chaikin 算法的规则。具体来说边界顶点的新位置是V (1/8) * V_prev (3/4) * V (1/8) * V_next其中 V_prev 和 V_next 是沿边界方向相邻的两个顶点。这个规则能保证边界曲线光滑且不收缩。另一个坑是非流形边被三个以上面共享的边。Catmull-Clark 的标准公式假设每条边最多两个相邻面遇到非流形边会算错。处理办法是先把非流形结构拆开或者对这类边退化成简单平均。还有一个常见问题是法线更新。细分之后顶点位置变了法线必须重新计算。如果你用的是顶点法线的平均记得按面积加权否则小三角形会主导法线方向导致光照出现奇怪的斑块。3. 曲面简化从高模到低模的取舍3.1 简化的目标与评价标准简化不是随便删面它有一个明确的优化目标在给定面数预算下让简化后的网格尽可能接近原始网格。这个接近怎么衡量就是各种算法拉开差距的地方。常用的误差度量有两种。一种是几何误差用简化后网格上的点到原始网格的最近距离Hausdorff 距离来衡量。另一种是属性误差包括法线偏差、UV 偏差、颜色偏差。实际项目里几何误差是基础但视觉上属性误差往往更明显。简化的评价标准可以总结成三条面数降下来了、形状没怎么变、拓扑没坏掉。第三条经常被忽略但很重要。有些简化算法会把网格搞出孔洞、自交或者翻转的法线这种模型后续没法用。3.2 边塌缩最主流的简化策略边塌缩Edge Collapse是工业界用得最多的简化方法。它的操作很直观选一条边把它的两个端点合并成一个新顶点所有连接到这两个端点的面重新连接到新顶点上。一次塌缩减少一个顶点、两条面对于三角网格。整个简化过程就是不断重复选边、塌缩这个动作直到达到目标面数。关键在于怎么选边。选得不好模型会变形严重选得好面数降一半视觉上都看不出来。最经典的选边策略是QEMQuadric Error Metrics二次误差度量由 Garland 和 Heckbert 在 1997 年提出。它的核心思想是对每个顶点维护一个二次误差矩阵 Q表示这个顶点到它周围所有平面的距离平方和。塌缩一条边时新顶点的误差是两边 Q 矩阵之和作用在新位置上的结果。选误差最小的边先塌缩。QEM 的数学形式Q(v) sum over planes (p) of (p^T * v)^2其中 p 是平面方程 (a, b, c, d)满足 a^2 b^2 c^2 1。把每个平面的贡献写成 4x4 矩阵 KpKp [a*a, a*b, a*c, a*d] [a*b, b*b, b*c, b*d] [a*c, b*c, c*c, c*d] [a*d, b*d, c*d, d*d]顶点的 Q 矩阵就是所有相邻平面 Kp 的和。塌缩边 (v1, v2) 时新顶点的 Q 是 Q1 Q2误差是 v^T * (Q1 Q2) * v。最优的新顶点位置可以通过求解 Q 的梯度为零得到如果矩阵不可逆就退而求其次在 v1、v2 和中点里选误差最小的。3.3 QEM 简化的完整实现下面是一个可运行的 QEM 简化实现用 Python 写依赖 numpy。为了控制篇幅只保留核心逻辑。import numpy as np import heapq def compute_quadric(vertices, faces): 为每个顶点计算 Q 矩阵 n len(vertices) Q [np.zeros((4, 4)) for _ in range(n)] for f in faces: v0, v1, v2 vertices[f[0]], vertices[f[1]], vertices[f[2]] normal np.cross(v1 - v0, v2 - v0) norm np.linalg.norm(normal) if norm 1e-12: continue normal / norm d -np.dot(normal, v0) a, b, c normal Kp np.array([ [a*a, a*b, a*c, a*d], [a*b, b*b, b*c, b*d], [a*c, b*c, c*c, c*d], [a*d, b*d, c*d, d*d] ]) for vi in f: Q[vi] Kp return Q def optimal_position(Q): 求解 Q 矩阵对应的最优顶点位置 A Q[:3, :3] b -Q[:3, 3] try: v np.linalg.solve(A, b) if np.all(np.isfinite(v)): return v except np.linalg.LinAlgError: pass return None def edge_cost(v1, v2, Q1, Q2): 计算塌缩边的误差 Q Q1 Q2 v_opt optimal_position(Q) if v_opt is not None: vh np.append(v_opt, 1.0) cost vh Q vh return cost, v_opt # 退化情况在端点和中点里选 candidates [v1, v2, (v1 v2) / 2.0] best_cost float(inf) best_pos None for c in candidates: ch np.append(c, 1.0) cost ch Q ch if cost best_cost: best_cost cost best_pos c return best_cost, best_pos def simplify_qem(vertices, faces, target_faces): QEM 简化主流程 vertices np.array(vertices, dtypefloat) faces [list(f) for f in faces] Q compute_quadric(vertices, faces) # 建立顶点到面的映射 vert_faces {} for fi, f in enumerate(faces): for v in f: vert_faces.setdefault(v, set()).add(fi) # 建立边集合 edges set() for f in faces: n len(f) for i in range(n): v1, v2 f[i], f[(i 1) % n] edges.add((min(v1, v2), max(v1, v2))) # 优先队列 heap [] for (v1, v2) in edges: cost, pos edge_cost(vertices[v1], vertices[v2], Q[v1], Q[v2]) heapq.heappush(heap, (cost, v1, v2, pos)) alive [True] * len(vertices) vertex_map list(range(len(vertices))) # 并查集式的映射 current_faces len(faces) def find(v): while vertex_map[v] ! v: vertex_map[v] vertex_map[vertex_map[v]] v vertex_map[v] return v while heap and current_faces target_faces: cost, v1, v2, pos heapq.heappop(heap) r1, r2 find(v1), find(v2) if r1 r2 or not alive[r1] or not alive[r2]: continue # 合并 r2 到 r1 vertex_map[r2] r1 alive[r2] False vertices[r1] pos Q[r1] Q[r1] Q[r2] # 更新受影响的面 affected vert_faces.get(r2, set()) | vert_faces.get(r1, set()) removed 0 for fi in list(affected): f faces[fi] new_f [r1 if find(v) r2 or find(v) r1 else find(v) for v in f] # 去重 dedup [] for v in new_f: if not dedup or dedup[-1] ! v: dedup.append(v) if len(dedup) 1 and dedup[0] dedup[-1]: dedup.pop() if len(dedup) 3: removed 1 faces[fi] None else: faces[fi] dedup current_faces - removed # 重新计算受影响边的代价 for fi in affected: if faces[fi] is None: continue f faces[fi] n len(f) for i in range(n): a, b find(f[i]), find(f[(i 1) % n]) if a b: continue key (min(a, b), max(a, b)) c, p edge_cost(vertices[a], vertices[b], Q[a], Q[b]) heapq.heappush(heap, (c, a, b, p)) # 整理输出 faces [f for f in faces if f is not None] used sorted(set(v for f in faces for v in f)) remap {old: new for new, old in enumerate(used)} new_vertices vertices[used] new_faces [[remap[v] for v in f] for f in faces] return new_vertices, new_faces这段代码跑一个几千面的模型降到一半面数通常几秒钟能出结果。实际工程里会用 C 重写加上空间加速结构处理百万面模型也没问题。注意QEM 的优先队列里会有大量过期条目因为顶点合并后旧边的代价失效了代码里用 find 和 alive 做了过滤。这是标准做法不要试图在每次合并后清理整个队列代价太高。3.4 简化中的拓扑保护与属性保持纯几何的 QEM 有个问题它只看几何误差不管拓扑。结果就是可能把薄壁结构的两面塌缩到一起或者把边界塌缩掉导致模型出现孔洞。解决办法是加拓扑约束。常见的约束包括不允许塌缩边界边或者给边界边加很高的惩罚权重不允许产生非流形边不允许翻转法线塌缩后相邻面的法线方向变化超过阈值就拒绝不允许产生退化三角形面积小于阈值这些约束在代码里就是几个 if 判断但能极大提升简化结果的质量。我个人的经验是边界保护是必须的其他约束看模型类型决定。属性保持方面如果模型有 UV 或者顶点颜色QEM 需要扩展成多维形式。做法是把 UV 坐标也当成几何坐标的一部分在计算平面时同时考虑位置和 UV。这样塌缩时 UV 的误差也会被最小化。不过要注意UV 的尺度通常和几何尺度不一样需要先归一化否则其中一个会主导误差。3.5 其他简化方法简述除了边塌缩还有几种简化思路值得了解。顶点聚类Vertex Clustering把空间划分成网格落在同一个格子里的顶点合并成一个。速度极快但质量一般容易出现锯齿状的边界。适合做 LOD 的粗层级。顶点删除Vertex Decimation逐个删除顶点然后对留下的孔洞重新三角化。适合有明确特征保留需求的场景但重新三角化比较麻烦。区域合并Face Merging把近似共面的相邻面合并成一个大面。适合建筑、机械这类平面多的模型对有机模型效果不好。实际项目里边塌缩 QEM 是默认选择其他方法在特定场景下作为补充。4. 细分与简化的配合使用4.1 什么时候该细分什么时候该简化这两个操作不是对立的而是同一套工具链里的两个环节。典型的工作流是这样的美术做一个中等面数的模型需要高精度渲染时运行时细分 1 到 2 轮需要远距离渲染时用简化生成 LOD 层级根据摄像机距离动态切换所以细分和简化经常出现在同一个项目里只是作用在不同的阶段。判断标准很简单如果模型面数低于目标精度就细分如果高于目标精度就简化。但要注意细分增加的面是均匀的而简化减少的面是有选择性的。所以如果你的模型某些区域需要高精度、某些区域不需要直接简化比先细分再简化更高效。4.2 细分后简化的陷阱有人会想我先细分得到一个光滑模型再简化它是不是能得到一个又光滑又轻量的模型理论上可以但实操里有坑。细分后的模型顶点分布很均匀QEM 简化时误差度量会倾向于均匀地删面结果就是光滑度下降得很快。而且细分引入的新顶点位置是计算出来的它们的 Q 矩阵可能不够准确导致简化时误差估计偏差。更合理的做法是在原始模型上直接简化保留原始顶点的 Q 矩阵信息。如果确实需要光滑效果可以在简化后的模型上再做细分或者用置换贴图来补充细节。4.3 动态 LOD 中的实际参数做动态 LOD 时细分和简化的参数需要配合调整。下面是一个实际项目里用过的参数表供参考。距离范围细分轮数目标面数比例说明0-5 米2100%近景全精度5-15 米160%中景适度简化15-30 米030%远景大幅简化30 米以上010%极远最低精度这个表不是固定的要根据模型复杂度、屏幕分辨率和性能预算调整。关键原则是距离越远简化越狠细分越少。因为远处模型在屏幕上占的像素少细节看不出来。实操心得切换 LOD 层级时如果两个层级的顶点位置差异太大会出现明显的跳变。解决办法是在切换时做一个短时间的顶点插值过渡或者让相邻层级的简化结果在边界处保持一致。这个在角色模型上尤其重要处理不好角色会抖。5. 常见问题与排查实录5.1 细分相关问题速查问题现象可能原因排查方法解决方案细分后模型收缩明显逼近型算法的固有特性对比细分前后包围盒预先放大原始模型补偿或改用插值型边界出现锯齿或扭曲边界顶点更新规则错误检查边界边是否被当作内部边处理单独实现边界规则细分后法线方向错乱法线未重新计算或加权错误可视化法线按面积加权重新计算面数爆炸导致卡顿细分轮数过多统计面数增长控制轮数或用自适应细分奇异点附近出现尖刺高价顶点更新公式不稳定检查顶点价数分布限制价数或对奇异点特殊处理5.2 简化相关问题速查问题现象可能原因排查方法解决方案简化后出现孔洞边界边被塌缩检查边界保护逻辑禁止塌缩边界边模型出现自交薄壁结构两面塌缩到一起可视化检查加拓扑约束检测自交简化后形状偏差大QEM 权重不合理对比 Hausdorff 距离调整 Q 矩阵加属性权重简化速度慢优先队列过期条目太多统计队列大小用惰性删除定期重建队列简化后 UV 错乱UV 未参与误差度量检查 UV 是否被更新把 UV 纳入 Q 矩阵计算5.3 几个踩过的坑坑一细分后顶点顺序变了导致动画出错。如果你的模型有骨骼绑定细分会插入新顶点这些新顶点需要分配骨骼权重。常见做法是用原始顶点的权重插值但插值方式要和细分规则一致否则动画会扭曲。我见过有人直接用最近邻结果关节处出现明显的撕裂。坑二简化时忽略了硬边。模型上的硬边比如立方体的棱如果被简化掉会变成圆角视觉上完全变样。解决办法是在 QEM 里给硬边加惩罚权重或者在简化前标记硬边简化时保护它们。判断硬边的方法是比较相邻面的法线夹角超过阈值比如 30 度就认为是硬边。坑三细分和简化的坐标系不一致。如果模型经过了非均匀缩放细分和简化的误差度量会失真。比如 X 轴缩放 10 倍Y 轴不变那么 X 方向的误差会被放大 100 倍。解决办法是在操作前把模型变换到均匀坐标系操作完再变换回去。坑四浮点数精度问题。大规模简化时顶点位置反复计算浮点误差会累积。表现是简化到后面模型出现细小的裂缝或者重叠。解决办法是用双精度浮点或者在每次塌缩后对顶点位置做一次量化对齐。6. 工具与库的选择建议如果你不想从零实现可以直接用现成的库。下面几个是我用过觉得靠谱的。OpenMeshC 库提供了半边数据结构和基本的细分、简化接口。文档一般但代码质量高适合嵌入到自己的项目里。CGAL功能极其全面包含各种细分和简化算法还有拓扑修复工具。缺点是编译慢、学习曲线陡适合对正确性要求极高的场景。libiglC 头文件库接口简洁和 Eigen 配合得很好。细分和简化都有实现适合快速原型。OpenSubdiv专注细分支持 Catmull-Clark 和 Loop有 CPU 和 GPU 实现。如果你只需要细分这个是最专业的选择。MeshLab图形界面工具内置了 QEM 简化和多种细分算法。适合做实验和快速验证不适合集成到代码里。选哪个取决于你的需求。做研究或者需要深度定制自己实现做产品需要稳定可靠用成熟库。我个人的习惯是核心算法自己实现一遍理解原理实际项目用成熟库保证效率。7. 性能优化的一些经验细分和简化的性能瓶颈不一样。细分主要是内存带宽受限因为要频繁读写顶点和面数据简化主要是计算受限因为要反复计算误差和更新优先队列。细分的优化方向用 SoAStructure of Arrays布局代替 AoS提高缓存命中率用并行计算处理面点和边点的生成因为它们是独立的对于 GPU 细分用曲面细分着色器Tessellation Shader直接在渲染管线里做。简化的优化方向用空间划分比如八叉树加速最近邻查询用更紧凑的数据结构存储 Q 矩阵对称矩阵只需要 10 个浮点数优先队列用配对堆或者斐波那契堆代替二叉堆降低更新代价。还有一个通用技巧分块处理。把大模型切成小块每块独立细分或简化最后合并。这样能控制内存峰值也方便并行。但要注意块边界处的连续性需要在边界留一圈重叠区域处理完再裁剪。8. 一些延伸方向细分和简化只是几何处理的入口往下还有很多可以挖的东西。自适应细分根据曲率或者屏幕空间误差只在需要的地方细分。这样能在相同面数下获得更好的视觉效果。实现难点是处理不同细分层级之间的过渡常用的方案是红绿三角剖分。保特征简化除了硬边还有特征线、特征点需要保护。可以用特征检测算法先提取这些结构然后在简化时给它们加约束。基于机器学习的简化用神经网络预测哪些边应该优先塌缩或者直接生成简化后的网格。这个方向最近几年比较热但工业界还没大规模应用。细分曲面的精确求值不实际生成细分网格而是直接在极限曲面上采样。这样能避免面数爆炸适合做高质量渲染。OpenSubdiv 里有实现。这些方向每一个都够写一篇长文这里就不展开了。如果你对某个方向感兴趣可以顺着关键词去查论文细分领域经典论文不多翻一遍就能把脉络理清。我个人在实际操作中的体会是细分和简化看起来是数学问题但真正决定成败的往往是工程细节。公式背得再熟边界处理没做好结果就是不能用。所以建议大家在学原理的同时一定要动手跑代码用真实的模型去试踩过坑之后理解才会深刻。另外不要迷信某一种算法QEM 不是万能的Catmull-Clark 也不是所有场景都合适根据你的模型特点和性能预算去选必要时组合使用才是务实的做法。