
如果你是一个游戏开发者,那么3D 多边形模型已经成为你日常生活中的一部分,并且你一定对一些3D概念例如每秒多边形数量、低面模型以及细节层次等等非常熟悉了。你可能也同样知道多边形减面算法的目的在于通过一个有着大量多边形的高细节的模型生成一个多边形数量比它少、但是看起来却跟原模型很相像的低面模型。这篇文章解释了一种实现自动减面的方法,并且附带的讨论了多边形减面的有用之处。在我们开始之前,我建议你去下载我的一个程序:BUNNYLOD.EXE,它展示了我将要阐述的这项技术。你可以在Game Develop网站上找到它。 在深入这个很“牛X”的3D算法之前,你可能会问你自己真的有必要关注它吗?要知道,已经有一些商业的插件和工具来为你减少多边形数量了。 然而,下面的几条理由会告诉你为什么需要实现自己的减面算法: 你使用的多边形减面工具生成的结果无法满足你的特殊需求,因此你希望做一个自己的工具。 你当前使用的多边形减面工具可能无法产生减面过程中的变化信息,而你却希望利用这些变化信息来使不同的细节层次之间的转换更加平滑。 你希望将生产过程自动化,这样的话美术人员就仅仅需要创建一个细节适当的模型,然后游戏引擎就能自动创建模型其余的细节层次。 你正在制作一个VRML浏览器,你希望提供一个菜单项来简化那些巨大的VRML文件。那些把这些巨大的文件放到网上的超级计算机用户没有想到这些文件在普通家用电脑上显示帧速率会比较低。 你在你的游戏中使用的特效改变了物体的几何形状,增加了多边形的数量,你需要一个方法来使你的引擎能够实时地快速减少多边形数量。
你对此怀疑?图1展示了一个具体的实例,一个游戏引擎对减少多边形这种特性的需求。  图1 爆炸效果对多边形数量的影响
在Bioware,我实现了实时的爆炸效果,并且把它们应用在了我们开发的一个游戏原型上,以便给我们的出版商留下深刻印象。玩家可以射击和爆破他们瞄准的实心物体表面的任意块。通过子弹撞击而改变游戏环境比典型的“定点爆破”这种只能在游戏世界中改变预先设定项的技术更棒。遗憾的是,重复不断的使用爆破效果会在物体上产生大量附加的三角形,如同你在图1中看到的一样。许多添加的面是很小的或者是碎片,不会对游戏的视觉效果产生丝毫的影响——它们仅仅是让游戏更慢。这种情况下就要求有实时的多边形减面功能,所以我开始寻找一种能够高效地完成这项工作的算法。 在我着手处理这个问题之前,我跟亚伯达大学图形实验室的一些人学习了多边形减面。(它让我跟一个团队一起工作,从而弄明白这个非常难的算法是如何工作的,并且弄明白什么样的技术适用于什么样的任务。)最近这个领域出现了很多研究成果,但其中大多数比较好的技术都是 H.Hpppe 的渐进网格算法的改进和变形(参见“更多的信息”)。那些技术都是通过重复不断的使用一个简单的边坍塌操作来降低模型的复杂度,见图2。  图2 边塌陷
在这个操作里面,u和v两个顶点(边uv)被选中并且其中一个顶点(这里是u)“移动”或者说“坍塌”到另一个顶点(这个例子里是v)。下面这些步骤说明如何实现这个操作: 1. 去除所有既包含顶点u又包含顶点v的三角形(换一种说法,去除所有以uv为边的三角形)。 2. 更新所有剩下的三角形,把所有用到顶点u的地方都用顶点v代替。 3. 移除顶点u。 重复以上的过程,直到多边形的数量达到了预期数量。每一次重复的过程中,通常会移除一个顶点、两个面、三条边。图3展示了一个简单的例子。  图3 多边形经过一系列边坍塌之后减少了面数
要产生效果比较好的底面模型的诀窍在于要正确地选择坍塌的边,能够在坍塌的时候最小程度的影响模型的视觉变化。研究者提出了各种各样的方法来使在每一次坍塌的时候能够选择出“最小影响”的边。但遗憾的是,最好的那种方法非常非常复杂(也就是说,很难实现),并且要花大量时间用于运算。因此这推动我要找到一种能够在游戏运行阶段减少多边形面数的方法,我做了很多实验,最后终于为这个选择边的过程开发了一种简单又超快的方法来生成相当不错的低面模型。 显然,先要去除那些小细节。同时要注意的是,对于那些在同一平面上的表面,只需要很少的多边形就可以表示,同时高度弯曲的曲面则需要更多的多边形来表示。根据以上这些,我们定义了:一条边是否要坍塌,取决于它的边长与曲率值的乘积。为了找到在uv方向上距离别的三角形最远的u的临接三角形,我们通过比较两个面的法线的点积得到坍塌边uv的曲率值。方程式1展现了用更多正式符号表示的求边坍塌值的公式。详见源码(你可以在Game Developer网站上下载到源代码)。  Tu是包含顶点u的三角形的集合,Tuv是同时包含顶点u和顶点v的三角形的集合。 方程式1 求边坍塌值的方程式
你可以看到,这个算法在决定哪一条边坍塌的时候对于面的曲率和大小做了平衡。要注意的是顶点u到v的坍塌值不一定和顶点v到u的坍塌值相同。此外,这个公式对于脊状的边的坍塌也是有效的。即使这条脊有可能是一个锐角,或者是直角,都没有关系。图4举例说明了这种情况。非常明显的,在平面区域中间的顶点B,可以被坍塌到顶点A或者顶点C。角上的顶点C应该最后被保留下来。如果把上面的顶点A坍塌到内部的顶点B,那就会非常糟糕。不过,顶点A可以沿着脊坍塌到顶点C,这丝毫不会影响这个模型的外观。  图4 好的和差的边坍塌
如果你正在实作你自己的减面算法,你可能希望能够用这个公式做实验,来看看是否满足你的要求。例如,对于一个动画模型,你可能希望能够改进公式,使它能够在判断潜在的坍塌边的时候可以参考不止一个动画关键帧的数据。如果对于你来说,模型质量比减面算法所需要的执行时间更重要的话,你应该考虑使用Hoppe的函数。我们已经添加了很多扩展用来处理贴图坐标、顶点法线、邻接边,以及表面断裂(比如贴图接缝)。 先显示一个原来的模型,然后显示简化后的模型,这是对多边形减面算法效果的最好证明。大多数的研究论文都用非常高面高细节的模型减面来证明它们的效果,原始模型接近100,000个多边形,简化后的模型只有10,000个多边形。对于3D游戏来说,更恰当(并且跟有挑战性)的测试是生成一个只有几百个多边形的模型,以此展示算法的强大威力。  图5 453个、200个以及100个顶点的小兔子模型(从左到右)
 图6 随机选择坍塌边(200个顶点)
举个例子,图5展示了一个小兔子的模型,它是从一个由 Viewpoint Datalabs 制作的VRML文件中提取出来的。模型的最初版本(左边)包含有453个顶点和902个多边形。后边显示的是减少到200个顶点(中间)和100个顶点(右边)的模型。希望你能够对图中不同数量多边形模型的视觉外观看起来感到满意。图6展示了由于没有选择出正确的坍塌边而简化的模型,这里坍塌边的选择是随机的。  图7 一个女性的人物模型,左边100%多边形数量,中间20%多边形数量,右边是4%多边形数量
当我们完成了动物实验之后,就要开始把这种算法应用在人物模型上了。图7展示了一个Bioware制作的女性人物模型的三个版本——4,858;1,000以及200个顶点。(根据欧拉公式,我们知道多边形的数量大致为顶点数的两倍。)这些模型图片是用平坦的方式渲染的,你能够明显的看到模型之间的不同之处。当我们使用平滑的方式渲染并且应用上贴图的话,那么这些差别就不会那么明显了。 我们最初的目标比较简单:我们想要找到一种方法可以减少由于过多的爆破特效造成的过多的多边形。但是,经过开发这个多边形减面算法并且在人物模型上得出的比预期好的结果,我们觉得这个技术完全可以用于在游戏引擎中生成模型的细节层次(LOD)。预计这个在基本算法基础上改进的新的版本可以整合进Bioware的3D引擎中。现在,我们的美术人员只需要为每一个游戏中的物体创建一个细致的模型就可以了。一个预处理的过程就可以为模型减面。然后,如果游戏每秒的帧速率低于预定的限度,或者游戏中的一个物体离摄像机相当远的时候,我们就可以拿一个低面的模型来代替高细节的模型。可以在游戏运行期间来做这些事情从而增加游戏的可伸缩性。游戏可以根据当前运行系统的马力来调整这些东西。 这种算法仅仅能够运用于三角形。如果需要的话可以把其它更多边的多边形简单地分解为三角形,除了这点就没有别的限制了。事实上,许多应用只用三角形。 大多数储存多边形物体的数据结构都是用一组顶点数据和一组三角形数据组成,其中三角形数据中包含了指向顶点数据的顶点索引数据。比如说: Vector vertices[]; class Triangle { int v[3]; // indices into vertex list } triangles[]; VRML中使用的索引面集合节点数据是这种数据结构的另一个例子。当一个物体中的两个三角形有相同的顶点的时候,它们有相同的索引值(因此它们共享顶点列表中的相同的顶点)。 class Triangle { public: Vertex * vertex[3];// the 3 points that make this tri Vector normal; // orthogonal unit vector Triangle(Vertex *v0,Vertex *v1,Vertex *v2); ~Triangle(); void ComputeNormal(); void ReplaceVertex(Vertex *vold,Vertex *vnew); int HasVertex(Vertex *v); }; class Vertex { public: Vector position; // location of this point int id; // place of vertex in original list List<Vertex *> neighbor; // adjacent vertices List<Triangle *> face; // adjacent triangles float cost; // cached cost of collapsing edge Vertex * collapse; // candidate vertex for collapse Vertex(Vector v,int _id); ~Vertex(); void RemoveIfNonNeighbor(Vertex *n); }; List<Vertex *> vertices; List<Triangle *> triangles; |
class Triangle { public: Vertex * vertex[3];// the 3 points that make this tri Vector normal; // orthogonal unit vector Triangle(Vertex *v0,Vertex *v1,Vertex *v2); ~Triangle(); void ComputeNormal(); void ReplaceVertex(Vertex *vold,Vertex *vnew); int HasVertex(Vertex *v); }; class Vertex { public: Vector position; // location of this point int id; // place of vertex in original list List<Vertex *> neighbor; // adjacent vertices List<Triangle *> face; // adjacent triangles float cost; // cached cost of collapsing edge Vertex * collapse; // candidate vertex for collapse Vertex(Vector v,int _id); ~Vertex(); void RemoveIfNonNeighbor(Vertex *n); }; List<Vertex *> vertices; List<Triangle *> triangles; | |