1. 背景
在家电模型的三维渲染场景下,我们经常会遇到模型内存占用过大进而导致系统内存崩溃的问题。这对设计工具来说一直是一个痛点,因为尽管内部模型库已经积攒了大量优质模型,但我们却无法把它们搬到三维渲染场景里使用,或者更确切的说是:无法完整的搬过来。因为,我们可以总是可以舍弃一些细节,用降低品质来换取内存的降低。具体舍弃什么类型的细节,是一个很大的话题,本文我们只讨论“模型面数”这一细节。
对模型面数进行缩减的方法已经十分成熟,不管是建模软件(如 3dsMax)还是单独的工具(如 PolygonCruncher)都能很好的完成工作。但是,在实际操作过程中我们会遇到一个棘手的问题:破面。一旦我们遇到破面问题,我们就不得不停止当前的缩减工作,让减面的比例恰好停留在“不会破面”的这一时刻。但是,此时模型的面数还远远达不到我们预期的水准,举个例子原本有 150w 面的冰箱,可能在 100w 面时就开始破面了,而我们预期的面数是 1w 面以下,这还差着 2 个数量级呢。
图1:冰箱减面
虽然我们可以使用更优秀的 PolygonCruncher 进行减面,可能它在 50w 面时依然不会破面,但由于它们实现算法基本一致,所以最终结果都会比预期大一个数量级,这依然是无法接受的。
所以,为了解决这个问题,我们必须要打开减面工具的“黑盒”,搞清楚 【问题 1】为什么会破面,然后在此之上,尝试寻找该问题的解法。
2. 基于 QEM(Quadric Error Metrics)算法的减面
目前业界大部分减面工具(包括上面提到的)都是基于这篇论文(Surface Simplification Using Quadric Error Metrics)设计的。这篇论文引入了 Quadric Error Metrics(二次误差,下称 QEM)这一度量值,来衡量一个点与该点周围面的一种拓扑关系的紧密程度。这个值越大,代表这个点与周围面的拓扑关系越紧密,反之关系越疏远。这也就给出了减面的依据和优先级——我们应该优先把那些不影响周围面拓扑结构的点删掉。
但是,我们的目的是减少面,删点是不够的。实际上我们不是遍历点并删点,而是遍历边并收缩边,下图是一个直观的展示:
图2:边的收缩
所以,所谓的减面,在 QEM 算法中实际上是收缩边的同时,合并该边两侧的三角面。那么为什么会出现破面, 【答案 1】就是因为我们合并了不该合并的三角面,破坏了物体的拓扑结构。举个例子:
减面过程
破面前一刻
破面后
上图中,我们使用 16 个平面组合成了一个板子,当我们不断对这个板子的三角面进行减面操作时,破面就会发生。我们不难看出,在破面前一刻,这个板子的 16 个平面已经是最简化的状态了(只有两个三角形),当我们再进一步时,会发现是下左图中 1 和 2 两个三角形被合并了,此时这个平面的拓扑结构就被破坏了,就产生了破面。
预期2和3合并,实际1和2合并
预期2和3合并的结果
但是如果我们能够像上右图一样,让三角面 2 和 3 进行合并,就不会产生破面了。
所以,破面产生的根本原因就是合并了不该合并的三角面,那么只有 【问题 2】搞清楚 QEM 算法 在选取待合并的三角面时的逻辑,我们才能找到解决破面问题的方法。
注意,收缩边和合并面是等价的,文中在设计“合并三角面“和“收缩边”的描述时,都是指同一件事。
3. 深入探究 QEM 算法
在这之前,首先再思考一个问题:为什么我们是通过遍历边来进行面的合并,而不是遍历点?因为,在三角形组成的三维网格中,每条边都一定会关联两个三角面,不多也不少,而三角面的合并最小粒度就是 2 个面合并,所以以边为粒度进行遍历,是最便于计算的。在明白这个最基础的前提后,我们正式开始 QEM 算法的分析。
我们知道所谓减面本质上就是收缩边,因为一条边对应着两个三角面,当这条边消失后,这两个三角面也就变成了一个三角面。这个过程其实很简单,如上方“图 2:边的收缩”一图所示,无非就是把原本一条边的两个顶点合并成一个新的顶点,然后删掉原来的两个老顶点,最后再把受影响的三角形的顶点重新设置到这个新的顶点上。
所以,收缩不是难事,难得是如何确定哪个边要进行收缩。在 QEM 算法中,它设计了一个称为二次误差(Quadric Error)的度量值(Metrics),并且为三维网格中每条边都赋予了这么一个 QEM,而 QEM 实际上就是一个浮点数,它是可以直接比较的。所以,当我们把带有 QEM 的每条边输入到一些排序容器里(比如最小堆)后,【回答 2】我们就能依次按照误差这个度量值作为优先级,渐进式的缩减我们的三维网格了,如下图所示:
在上方这个兔子的减面过程中,我们可以发现其轮廓被很好的保留下来了,也就是轮廓上这些边的 QEM 是很大的,大到它们直到最后才会被收缩。而上文中矩形减面的例子中,我们却发现轮廓被提前收缩了。 那么,要想搞清楚为什么会产生破面,或者说“为什么优先收缩了不该优先收缩的边”,就要 【问题 3】搞清楚 QEM 的计算逻辑。
点的 QEM 计算逻辑
QEM 虽然是边的属性,但是它并不能直接通过边来计算得到,而是要通过分别计算其两个端点的 QEM0 和 QEM1,然后通过 QEM0+QEM1 来得到。因此,我们要从三维网格的顶点说起。
QEM 的灵感来自于一个最基础的线性代数的概念:点到面的距离。也就是说 QEM 设法通过这个距离来衡量一个点的变动对于周围拓扑结构的影响程度。 具体来说,对于三维网格中的一个顶点,它到由三角形三个点组成的平面 的距离。其中,平面使用一个四维向量表示,前三项是平面的法线单位向量 n,第四项是平面到原点的距离 d,也就是用法线和距离表示一个平面。而顶点为了方便计算,也使用四维向量来表示,第四项是 1。
而上述点到面的定义下距离是:,而由于 n 是单位向量,所以 。这里,QEM 做了一个巧妙的转换,它取 distance 的二次方,即 ,然后根据点乘的交换律转换成: ,这里巧妙的点就是我们把关于平面的向量 p 给聚合到一起了。接着,对于向量的点乘我们可以表示成矩阵的乘法, ,其中表示两个平面向量 p 作为矩阵外积的结果,也是一个矩阵。
这样,我们就通过把点到面的距离的二次方给表示出来了,这也是 QEM 中的二次方 Q 的含义。接着,我们需要依次把这个点周围的所有三角面都计算一次,最后把计算结果加起来,最终得到一个可以衡量一点对周围拓扑结构影响程度的度量值。如下图所示:
其中 就表示在 v 点的误差值(Error),也就是 QEM 的 E。而所谓的 QEM 这个度量值其实就是指这里使用矩阵表示的误差值,同时我们发现 中 v 是已知的,因为我们本来就有点的位置信息, 【回答 3】所以所谓的 QEM 具体来说应该是指 这一部分,用一个符号来表示就是 Q。上面公式也就简化成 。
但你可能会问,直接用数值来表示距离不是更简洁吗,为什么偏要用矩阵形态来表示? 因为,点的 QEM 的计算最终是服务于边的 QEM,而边的 QEM 一方面用于优先级排序,另一方面也是最重要的——要用作收缩后新点的位置计算,即下图中边收缩后的新点具体位于什么位置:
是左边,还是右边,还是中间,还是 v1 和 v2 中间任何一个位置?如果误差值 只存单独的数值信息是没办法帮助我们计算空间位置的。但如果我们把面的信息(以矩阵形态)存储到误差值里,我们就有机会 【问题 4】反向计算出点的空间位置,具体我们下面说。
边的 QEM 计算逻辑
边的 QEM 计算很简单,因为矩阵是可以相加的,如果一边两点的 QEM 分别为 Q1 和 Q2,那么变得 QEM 就是 Q1+Q2。但是,矩阵是没法直接进行数值比较的,所以我们需要把 QEM 转成距离的形态:,具体来说就是把矩阵形态又变回向量形态,让 v 与矩阵 Q 跟向量 v 相乘的结果进行点乘,就得到了距离的二次方。这样我们就拿到优先级比较的依据了。
下一步,我们要计算收缩后新点的空间位置。这个空间位置可能有无数种可能,但是我们要选一个 QEM 误差值最小的情况。而我们已经知道了关于 v 的误差函数 ,而且这个函数是二次的(因为 vQv),那么寻找最小误差值的问题就是求二次函数极值的问题。
论文中给出了解法:
qij就是矩阵Q的每一项
【回答 4】结论是通过计算 Q 的逆矩阵,然后乘以一个单位向量,就得到了新的点 v 的空间位置。因为我们已经有了 Q,所以新的点的位置计算不管对于计算机还是对于程序设计来说,都是很容易得到的。这也就是为什么我们偏要用矩阵形态来表示距离的二次方的原因。
上述结论可以把 vQv 展开,然后对 求偏微分得到,具体可以查看论文,这里就不深入了。
轮廓的保护
由于 QEM 算法是基于边的二次误差这一度量值进行减面的,对于轮廓保护这一件事情来说,QEM 算法可以很容易的实现,只需要给属于轮廓的那些边以更高的权重,更具体来说就是更高的误差,从而让它们在减面过程中保持较低的优先级。
在实际应用时,轮廓的保护一般有三个级别:
- CrunchBorder:裁切轮廓,也就是不保护轮廓,轮廓上的边没有特殊权重
- ProtectBorder:保护轮廓,给轮廓上的边以更高的权重,但不保证轮廓一定不会被收缩
- ExcludeBorder:排除轮廓,直接把轮廓上的边排除到算法之外,不参与减面计算
以下是三种级别的直观展示:
CrunchBorder 裁切轮廓
ProtectBorder 保护轮廓
ExcludeBorder 排除轮廓
那么,具体来说这个权重如何跟 QEM 度量值结合起来也很简单,因为当前优先级对比的依据就是距离的二次方,已经是一个数值,那么我们基于这个数值按需再乘以一个 weight 即可。
但这里最关键的其实是 【 问题 5 】如何确定一个边是属于轮廓的? 其实很简单,前面说过“在三维网格中,每条边都一定会关联两个三角面”,但是当三维退化成二维时,如下图 terrian 瓦片所示,在边界出就会出现【回答 5 】一条边只关联一个三角面的情况,此时这条边就在轮廓上。
terrian瓦片边界处退化成二维
具体实现时,我们会借助 half-edge 数据结构,帮助我们组织好点、线、面的关系,从而加速算法的计算,这里就不过多赘述了。
4. 轮廓保护的失效原因及解法
现在再回到前面提到的问题: 【问题 6】为什么优先收缩了不该优先收缩的(轮廓)边?
根据上文的 QEM 算法,在设置 ProtectBorder 时,通过赋予轮廓的边以更高的权重,使其二次误差变大从而在优先级排序中靠后,这样来实现保护轮廓的能力。而事实也证明这个 ProtectBorder 能力是有效的,下图中 16 个平面已经各自减面到最优状态。
破面前一刻
破面前一刻左上角平面的非连接的点
但问题是,QEM 算法所识别出的轮廓,和我们理解的轮廓,存在偏差。 【回答 6】这是因为存在“非连接的点”(如右图所示),导致多个平面之间的三角面是分离的, 进而导致 QEM 认为左上角这个面的边已经是轮廓了(因为这些边只有一个与其相关的三角面)。所以,再进一步减面时,QEM 算法就不得不对这些轮廓上的边动手了。 【问题 7】那么为什么当 QEM 对轮廓动手后,就会产生破面呢?这个我们下面再说。
因此,为了准确让 QEM 识别出我们想要的轮廓,我们必须要对这个板子做一些模型修改,具体来说就是把分离的顶点“焊接(Weld)”在一起。
焊接两个分离的点
焊接后再进行合并
但是你可能会问既然 ProtectBorder 可能会导致边被合并,那为什么不该用 ExcludeBorder?因为,ExcludeBorder 会极大降低减面的效率,这可以从上文的 ExcludeBorder 动图里看到。
5. 破面的本质及优化思路
到这里,我们可以得到一个很重要的结论: 【回答 7】破面其实并不是面破了,而是面的轮廓被破坏了。QEM 算法永远不会让物体产生“洞”,之所以看起来出现了一些“洞”,是因为多个分离的物体,原本是“严丝合缝”的拼接在一起,但由于其中一个物体的轮廓遭到破坏,使得多个分离物体之间出现了“间隙”。
而为什么会产生这种破面,是因为 QEM算法只能保护单个轮廓。如果你的 【问题 8】模型存在多个轮廓,QEM 算法只会“分别”去保护这每一个轮廓,而不会考虑全局视角下的整体轮廓。以下方冰箱面板为例:
轮廓情况
破面分布情况
从上图可以明显看出,破面的分布情况跟轮廓的分布是一致的。
CAD 模型的问题
【回答 8】 之所以我们的模型有如此多的轮廓,是因为这些模型大部分都来自于 CAD 软件的直接导出,而不是人工一点一点建模的,这会导致两个问题,如下图所示:
轮廓边界处的情况,其中白色为正面,灰色为反面
从图中可以看到两个现象:
- 存在大量空间位置重叠的冗余点位:上图中 1 条边界线,实际上由 4 条边组成
- 存在大量正反双面:这 4 条边,2 条属于正面,2 条属于反面
所以问题有两个:
- 本该只有一条边的建模,实际上可能由 N 倍的边组成
- 本该只有一个面的建模,实际上可能有 2 倍的(正反)面组成
所以,看似简单的模型,实际上可能暗藏着 2N 倍的数据量。
优化思路 1——焊接点
那么,现在回到多轮廓问题上。虽然模型中存在多个轮廓,但是轮廓之间是紧密连接的,所以 【问题 9】 只要我们把轮廓边界上的点与周围的点焊接到一起,那么这个轮廓就消失了,是不是就可以解决问题了?
原始轮廓情况
焊接0.1mm后轮廓情况(红框标记处)
原始状态直接减面
焊接0.1mm后再减面
减面后渲染效果:左原始,右焊接后
焊接后正反面问题没有解决
可以看到,的确在焊接后,由于轮廓数量的大幅减少(依然残留几个),最终破面问题得到很大改善。
另外,在焊接后另外一个问题,如上右图所示—— 【问题 10】 正反面双面问题依然存在,这会影响减面的效率,我们后面再说。
但观察上图中减面后的渲染效果,可以看到,虽然破面问题改善了,但是在高比例(100-1.3%=98.7%)的减面下,背板的凹凸不平的细节被磨平了,这是为什么呢?
因为,轮廓在 QEM 减面过程中其实同时带来了正反两方面影响,反向影响就如上面所说,这些零碎的轮廓会在高比例减面下遭到轮廓破坏,进而导致模型从整体上看到一些破洞;但它也有正向的作用,那就是保护细节。因为在 ProtectBorder 机制下,轮廓上的边是最后被收缩的,这也就侧边保护了轮廓周边的细节。轮廓越多,就有越多的细节被保留。所以,当我们通过焊接大规模减少轮廓后,原本冰箱背板处那些位于轮廓边缘的细节就会跟普通的三角面一样平等的被合并,这些细节也就逐渐消失了,如下图所示:
焊接之前,两个轮廓
焊接之后,一个轮廓
可以看到,【回答 9】焊接的确可以彻底解决破面问题,只不过在高减面比例下,它无法保留原始轮廓边缘处的高低细节。但这其实不是问题,因为这已经是 QEM 算法的极限了,QEM 算法是基于二次误差作为依据进行收缩边的优先级排序,当你发现 QEM 开始对上图这种带有拐角的边下手时,说明模型已经没有其他更高优先级的边了。
关于焊接阈值:上文提到过,虽然我们进行 0.1mm 的焊接可以大幅减少零碎轮廓的数量,但是依然会有残留。这些轮廓必须要用更大的阈值才有可能合并起来,但是一味的提升阈值又会导致一些不该合并的点被合并。所以这个问题并不好解决,也许我们应该针对模型的种类总结出一些经验值。
优化思路 2——高模转低模
高模转低模是一个常见的模型优化方案,它通过把高模的三维网格细节通过渲染的方式,烘焙到一个法线凹凸贴图中,然后再把这个贴图贴到一个有更少细节的低模上,就可以用更少的三角面数来模拟出原来复杂的三维网格细节。这个思路已经脱离 QEM算法减面的范畴,所以这里就不展开了,等我们有机会再分享。以下是该优化思路初步实验的成果:
渲染效果:左高模,右低模
网格数量:左高模,右低模
可以看到无论是渲染效果,还是模型本身的一些细节(比如挖孔,轮廓)都很好的还原了原始模型。但最重要的是,这个低模是借助 OpenCV 技术自动生成的。
这个优化思路虽然不是“正道”,但它可以绕开减面遇到的破面问题和细节丢失问题,一步到位彻底解决模型三角面数过多的问题。
6. 正反面问题
上文提到,看似简单的模型,实际上可能暗藏着 2N 倍的数据量,其中的 2 就是正反面导致的。很容易想到,我们可以通过焊接点来把正反面重叠的点进行合并,但实际情况是:焊接只会对开放边界(open edge)上的相邻的顶点进行合并,这里的相邻并不是单纯指“空间距离”上的相近,也还包含着“空间方向”上的相近。因此焊接在机制上就无法合并这种正反双面上的点。
【回答 10】 所以,这个问题并不容易,最好的办法是直接删除正反两面中的其中一个面。这里有一个思路:遍历所有三角面,找出那些三个顶点坐标完全一样的面,删除其中一个。
7. QEM 算法对非流型的处理
有一个问题是: 【问题 11】QEM算法是支持非流型物体的,也就是支持对分离的多边形进行减面的,那么为什么没法很好的处理我们的家电模型? 没错,QEM 算法的确支持,但是这个支持是建立在一个前提——你要处理的模型你自己认为它是非流型的,比如人体脚掌的骨架:
原始模型
减面后
这个时候,你对减面的结果是没有异议的,因为即便在多个轮廓之间产生了缝隙,你也认为它是正常的。但是,如果你对一个“你认为是流型,但实际上是非流型流型”或者说“你认为只有一个轮廓,但实际上有多个轮廓”的模型进行减面,那么结果就会与预期不符。
【回答 11】 所以,QEM算法从来都不存在问题,不管是流型还是非流型。之所以我们对 QEM 算法减面的结果不认可,是因为我们对模型的拓扑形态的认知不足罢了。
8. 总结
到这里,我们已经对家电模型减面问题做出了完整的分析,接下来通过集合我们的问题和回答来作为总结:
问题 1:为什么会破面?
回答 1:因为我们合并了不该合并的三角面,破坏了物体的拓扑结构。
问题 2:QEM 算法 在选取待合并的三角面时的逻辑?
回答 2:按照二次误差这个度量值作为选取待合并三角面的依据和优先级。
问题 3:QEM 的计算逻辑是什么?
回答 3:,其中 Q 是一点周围所有三角面的平面向量外积的和,即 。
问题 4:如何通过 QEM 反向计算出合并点的位置?
回答 4:通过计算 Q 的逆矩阵,然后乘以一个单位向量,就得到了合并点的空间位置。
问题 5:如何确定一个边是属于轮廓的?
回答 5:一条边只关联一个三角面的情况,此时这条边就在轮廓上。
问题 6:为什么优先收缩了不该优先收缩的(轮廓)边?
回答 6:这是因为存在“非连接的点”,导致多个平面之间的三角面是分离的,QEM 认为这个面的边已经是轮廓了。
问题 7:那么为什么当 QEM 对轮廓动手后,就会产生破面呢?
回答 7:破面其实并不是面破了,而是面的轮廓被破坏了。
问题 8:为什么模型会存在多个轮廓?
回答 8:因为这些模型大部分都来自于 CAD 软件的直接导出,而不是人工一点一点建模的。
问题 9:把轮廓边界上的点与周围的点焊接到一起,就能解决破面问题吗?
回答 9:焊接的的确可以解决破面问题
问题 10:正反面双面问题如何解决?
回答 10:遍历所有三角面,找出那些三个顶点坐标完全一样的面,删除其中一个。
问题 11:QEM算法是支持非流型物体的,那么为什么没法很好的处理我们的家电模型?
回答 11:QEM 算法从来都不存在问题,不管是流型还是非流型。之所以我们对 QEM 算法减面的结果不认可,是因为我们对模型的拓扑形态的认知不足——我们认为家电是完整的、单轮廓的而实际上是细碎的、多轮廓的。
9. 参考
[1] 原始论文:www.cs.cmu.edu/~./garland/…
[2] 开源减面算法实现 1:github.com/zeux/meshop…
[3] 开源减面算法实现 2:github.com/matthew-ris…
10. 团队介绍
「三翼鸟数字化技术平台-筑巢自研平台」依托实体建模技术与人工智能技术打造面向家电的智能设计平台,为海尔特色的成套家电和智慧场景提供可视可触的虚拟现实体验。智慧设计团队提供全链路设计,涵盖概念化设计、深化设计、智能仿真、快速报价、模拟施工、快速出图、交易交付、设备检修等关键环节,为全屋家电设计提供一站式解决方案。