最简单的情形,多边形网格不过是一个多边形列表;三角网格就是全部由三角形组成的多边形网格。多边形和三角网格在图形学和建模中广泛使用,用来模拟复杂物体的表面,如建筑、车辆、人体,当然还有茶壶等。图14.1给出一些例子:
当然,任意多边形网格都能转换成三角网格,三角网格以其简单性而吸引人,相对于一般多边形网格,许多操作对三角网格更容易。
1 表示网格
三角网格为一个三角形列表,所以最直接的表示方法是用三角形数组:
Listing 14.1: A trivial representation of a triangle mesh
struct Triangle { Vector3 p[3]; };
struct TriangleMesh { int triCount; Triangle *triList; };
对于某些应用程序,这种表示方法已经足够。然而,术语"网格"隐含的相邻三角形的连通性却未在这种简单表示中有任何体现。实际应用中出现的三角网格,每个三角形都和其他三角形共享边。于是,三角网格需要存储三类信息:
- 顶点。每个三角形都有三个顶点,各顶点都有可能和其他三角形共享。
.
- 边。连接两个顶点的边,每个三角形有三条边。
.
- 面。每个三角形对应一个面,我们可以用顶点或边列表表示面。
1.1 索引三角网格
在索引三角网格中,我们维护了两个列表:顶点表(Vertex buffer)与三角形表(Index Buffer)。
每个顶点包含一个3D位置,也可能有如纹理映射坐标、表面法向量、光照值等附加数据。
每个三角形由顶点列表的三个索引组成。通常,顶点列出的顺序是非常重要的,因为我们必须考虑面的"正面"和"反面"。从前面看时,我们将用顺时针方向列出顶点。另外一些信息也存在这一级中,如预先计算的表面法向量,表面属性(纹理映射)等。
程序清单14.2给出了一段高度简化的代码: Listing 14.2: Indexed triangle mesh
// struct Vertex is the information we store at the vertex level struct Vertex { // 3D position of the vertex Vector3 p;
// Other information could include texture mapping coordinates, // a surface normal, lighting values, etc. }
// struct Triangle is the information we store at the triangle level struct Triangle { // Indices into the vertex list int vertex[3];
// Other information could include a normal, material information, etc. }
// struct TriangleMesh stores an indexed triangle mesh struct TriangleMesh { // The vertices int vertexCount; Vertex *vertexList;
// The triangles int triangleCount; Triangle *triangleList; };
实践中,三角网格类会有一系列方法,用于存取和维护顶点、三角形列表。当然,存储多边形网格,还需要定义一个多边形类,用来表达有任意多顶点的面。为简化和提高效率,我们可以对每个多边形的最大定点数做出限制。 注意到,索引三角形列表中的邻接信息是隐含的。例如:边信息没有直接存储,但我们还是可以通过搜索三角形表找出公共边。和前面"三角形数组"方式相比,这种方式确实能节省不少空间。原因是信息存于顶点级别,它的整数索引比之三角形数组里存储的顶点重复率要小得多。
1.2 高级技术
简单索引三角网格对于基本应用已经足够了,但为了更加高效地实现某些操作还可以进行一些改进。主要的问题是邻接信息没有显式表达,所以必须从三角形列表中搜索。另一种表达方法可以在常数时间内取得这种信息。
方法是维护一个边列表,每个边由两个端点定义,同时维护一个共享该边的三角形列表。这样,三角形可视为三条边而非三个点的列表,也就是说它是边列表而不是点列表的索引。该思想的一个扩展称作"winged edge"模型,对每一顶点,存储使用该点的边的索引。
2 针对渲染的特殊表达
大多数图形卡并不直接支持索引三角网,渲染三角形时,一般是将三个顶点同时提交。这样,共享顶点会多次提交,三角形用到一次就提交一次。因为内存和图形硬件间的数据传输是瓶颈,所以许多API和硬件支持特殊的三角网格式以减少传输量。基本思想是排序点和面,使得现存中已有的三角形不需要再次传输。
从最高灵活性到最低灵活性,我们讨论三种方案:顶点缓存,三角带,三角扇。
2.1 顶点缓存
与其说顶点缓存是一种特殊的存储格式,不如说是API和硬件之间的一种存储策略,用以发挥连续三角形顶点一致性的特点。通常,高级代码不需要了解顶点缓存是如何实现和执行的。
和其他缓存机制类似,顶点缓存基于最近使用的数据将来仍被使用的原则。图形处理器缓存一小部分(如,16个)最近使用的顶点,当API要发送顶点时,首先探测缓存内是否已存在。当然,这要求API了解图形卡缓存的大小和替换机制。若缓存内没有该顶点,则发生脱靶,API发送顶点并更新缓存;若缓存内有该顶点,就命中,API通知图形卡"使用缓存内位置x的顶点"。
顶点缓存其实是一种底层的优化手段,任何三角网都可用高级代码实现正确渲染而不用考虑缓存。但进行顶点顺序的调整,使共享顶点的三角形集中发送有助于提高效率。这种调整只需要进行一次,并且可以离线进行,它只会对性能有帮助,不会使没有缓存的系统性能降低。善用缓存,可能使发送到显卡的顶点数降低到平均每三角形少于一个。
2.2 三角带
三角带是一个三角形列表,其中每个三角形都与前一个三角形共享一边,图14.2显示了一个三角带的例子。
注意顶点列出的顺序使得每三个连续的点都能构成一个三角形。例如:
- 顶点1、2、3构成第一个三角形。
.
- 顶点2、3、4构成第二个三角形。
.
- 顶点3、4、5构成第三个三角形。
在图14.2中,顶点以构成三角形带的顺序编号。"索引"信息不再需要,因为顶点顺序已经隐式定义了三角形。通常,列表前部有顶点数目,或末尾处有一特殊码表示"列表结束"。
注意到,顶点顺序在顺指针和逆时针间不断变换(见图14.3)。某些平台上,需要指出第一个三角形的顶点顺序,而有些平台上顺序是固定的。
最佳情况下,三角带可用n+2个顶点存储n个面。n很大时,每个三角形平均发送一个顶点,遗憾的是,这只是最佳情况。实践中,很多网格是一个三角形带无法表达的,不仅如此,3个以上三角形共享的顶点还是要多次发送给图形卡。从另一方面说,每个三角形至少要发送一个顶点。但在顶点缓存机制中,有可能将每个三角形发送的顶点数降到一个以下。当然,顶点缓存需要额外的簿记信息(索引和缓存管理数据),可是尽管这些额外信息对单个顶点来讲相对较大,操作速度也会相对下降,但发送顶点数最少的系统在特定平台上速度最快。
假设用一种生成三角带的直接方法,用三角带表示三角网需要的顶点数为t+2s,t为三角形数目,s为三角带数目。每个三角带的第一个三角形对应三个顶点,以后每个三角形对应一顶点。因为我们希望最小化发往图形卡的顶点数,所以三角带的数目应尽可能少,即三角带越长越好。STRIPE方法给出了一种三角带数目接近理论下限的生成手段。
另一个希望减少三角形带数目的原因在于建立各三角形需要额外时间。从另一方面说,分别渲染两个长为n的三角带所需时间长于渲染一个长为2n的三角带,即使这个三角带中的三角形数多于两个分开带中三角形数量的和。于是,我们经常通过使用退化三角形连接多个三角带,从而将整个网格置于一个连续的三角带中,退化的意思是面积为0。图14.4显示了如何重复顶点以将两个三角形合并为一个。
图14.4的含义不太明显,但这里有四个退化三角形用于连接两个三角带从而维持正确的顺指针、逆时针顺序。顶点7、8间的边实际包含两个退化三角形,图14.5指出了图14.4中包含的三角形。
退化三角形面积为0不需要渲染,所以不会影响效率。实际上要发送到图形卡的顶点仍然只是第一列的顶点:
1,2,3,4,5,6,7,8,9,10,11,12,13
这符合我们每三个连续顶点表示一个三角形的约定。
一些硬件(如PS2上的GS)可以跳过三角带中的三角形,方法是通过一个顶点上的标志位指出"不必绘制"此三角形。这给我们一种方法可以有效的从任意点开始新三角形带而不必重复顶点或使用退化三角形。例如,图14.4中的两个三角带可以如图14.6那样连接,其中灰色表示顶点被标记"不必绘制"。
2.3 三角扇
三角扇和三角带类似,但不如三角带灵活,所以很少使用。图14.7所示即为三角扇。
三角扇使用n+2个顶点存储n个面,和三角带相同。但是,第一个顶点必须为所有三角形共享,所以实践中不太经常能找到大型三角扇应用的场合。并且,三角扇不能像三角带那样连接。所以,三角扇只能在特殊场合应用,对一般应用来说,三角带更灵活。
3 三角形网格可在三角形或顶点级保存额外信息。
3.1 纹理映射坐标
纹理映射是将位图(称作"纹理图"或简称"纹理")贴到多边形表面的过程。这里只给出一个高度简化的解释:我们希望将2D纹理贴到多边形表面上,同时考虑多边形在摄像机空间的方向。对多边形中每个需要渲染的像素都要计算2D纹理映射坐标,这些坐标用以索引纹理图,从而为相应像素着色。
通常,在顶点保存纹理映射坐标,三角形面中其余各点的坐标通过插值进行计算。
3.2 表面法向量
许多应用程序中,网格上的各点都需要一个表面法向量。它可以用来:
- 计算光照。
.
- 进行背面剔除。
.
- 模拟粒子在表面"弹跳"的效果。
.
- 通过只考虑正面而加速碰撞检测。
表面法向量可能保存于三角形级或顶点级,或两者皆有。
三角形级法向量可以通过两向量叉乘的方法轻松获得,而顶点级法向量的计算则困难一些。首先,应注意到顶点处其实是没有法向量定义的,因为此处网格表面不连续。第二,三角网是对连续表面的逼近,所以我们实际想要的是连续表面的法向量。根据产生三角网的方法,这种信息不一定现成可得。如果网格是自动生成的,比如说从参数曲面上,则可以直接获得法向量。
若法向量没有提供,则必得有现成数据(顶点位置和三角形)生成。一个技巧是平均相邻三角形的表面法向量并将结果标准化。当然,这要求知道三角形法向量。一般可以假设三角形顶点以顺时针列出,通过叉乘计算外表面的法向量。如果顶点顺序不能假设时,可使用Glassner建议的方法。
通过平均三角形法向量求得顶点法向量是一种经验性方法,大多数情况下都能工作得很好。但是有必要指出,某些情况下,其结果并不是所期望的。最明显的例子是两个法向量刚好相反的三角形共享一个顶点。这种情形常发生在"公告板"物体上。"公告板"由两个三角形背靠背构成,它的两个法向量方向恰好相反,其平均值为0不能标准化。为解决这种问题,必须拆开所谓的"双面"三角形。
平均顶点法向量的另一个问题会在应用Gouraud着色时发生,这里给出一个简化的解释:光照是按顶点法向量逐点计算的。如果使用平均三角形法向量计算的顶点法向量,某些应该有尖锐边缘的地方会显得"过于平滑"。以最简单的盒子为例,边缘处应该有一个剧烈的关照变化。如果我们使用平均顶点法向量,这个剧烈变化会消失。如图14.8所示:
根本问题在于盒子边缘不连续,而这种不连续却不能很好的被表达,因为每个顶点只有一个法向量。其实仍然可以使用面拆分解决问题:换句话说,重复不连续处的顶点。这样做之后,人为的构造了一个不连续以防止顶点法向量被平均。这种"裂缝"在网格拓扑中可能会导致问题,但在如渲染、光线追踪等任务中没有问题。
另一个小问题是这种平均会导致结果向较多拥有相同法向量的三角形偏移。例如,若干三角形共享一个顶点,但其中两个共面。则平均出的法向量会发生偏移,因为共面三角形的法向量重复了两次,相比于其他法向量有更多"发言权"。于是,即使表面并未变化,也会使顶点法向量发生改变。我们可以修正此错误,但幸运的是实践中这并不是什么大问题,因为顶点法向量本来就是一种近似。
3.3 光照值
另一种常由顶点维护的信息是光照值。这些光照值用于沿表面的插值,典型的方法是Gouraud着色。有些时候,顶点处仅保存法向量,渲染时动态计算光照值。
4 拓扑与一致性
三角网格的拓扑是指当在三角网格中不考虑顶点位置与其他几何性质的逻辑连通性时,两个顶点数相同且三角形互联方式一致的三角网格为同拓扑的,即使它们对应的物体完全不同。从另一方面说,尽管形状不同,拉伸网格但不打破邻接性,我们得到的是同拓扑的网格。
有一种特殊网格称作封闭网络,又称作"流形"。概念上,封闭网格完美地覆盖物体表面,网格中没有间隙,从外面完全无法看到任何三角形的背面。这是一种重要的网格,它的点和边组成形式就像平面图,即如果将顶点当成平面点,用直线连接顶点,此封闭网格可以画在一个2D平面上,而且没有边交叉。平面图符合Euler方程:v-e+f=2,其中v为顶点数,e为边数,f为网格上的面数。
实践中,我们经常遇到拓扑异常的三角网格,导致网格不封闭:
- 孤立顶点:顶点未被任何三角形使用。
.
- 重复顶点:完全相同的顶点。使用这些点的三角形几何上相邻而逻辑上不相邻,多数情况下,我们不希望看到这种现象,应该删除。
.
- 退化三角形:使用一顶点超过一次的三角形。意味着这个三角形没有面积,一般这种三角形应该删除。
.
- 开放边:仅为一个三角形所使用。
.
- 超过两个三角形共享的边:封闭网格中,任一边必须为两个三角形共享。
.
- 重复面:网格中包含有两个或更多相同的面。这是不希望看到的,应该去掉多余面而只保留一个。
.
根据应用的不同,上述异常可能是严重的错误,也可能是小错误,或者无关紧要。
5 逐三角形/点操作
三角网格是顶点和三角形的列表。三角网格的一系列基本操作都是逐点和逐三角形应用基本操作的结果。最明显的,渲染和转换都属于这种操作。为渲染三角网格,我们逐个三角形渲染,如要向三角网格应用转换,如旋转和缩放等,应逐顶点进行。
5.1 焊接顶点
当两个或更多顶点(也许有误差)时,将它们焊接在一起是有益处的。更加准确地说,删除其余的,只剩一个。例如,我们要焊接图14.9中的A和B,有两个步骤:
- 步骤1,扫描三角形列表,将对B的引用全部替换成对A的引用。
.
- 步骤2,现在B是孤立点,将它从顶点列表中删除。
焊接顶点的目的有两个。首先,去除重复顶点,节约内存。这是一种重要的优化方法,使得对网格的操作(如渲染和转换)更快。其次,使几何上相邻的边在逻辑上也是相邻的。
上面讨论的是两个顶点的焊接,实践中,我们常常希望找出与焊接点邻近的所有顶点。这个想法是非常直接的,但有几个细节需要明确。
(1)焊接前应去除孤立点,我们不想让任何未被使用的点影响正被使用的点,如图14.10所示:
(2)当两个顶点均来自"细长"三角形,焊接可能产生退化三角形,如图14.11所示(这和边缩坍类似)。这样的三角形应被删除,通常它们的数量并不大。焊接常会显著减少顶点数,同时也会除去一小部分细长面。
(3)焊接时,似乎应该用原顶点的平均作为新顶点,而不是简单地选择其中一个而抛弃另一个。这种方式不偏向任何一个顶点,,在只有少量顶点需要焊接时这似乎是个好主意。然而,焊接自动进行的时候可能引起"多米诺"效应,导致原来不在误差容限内的多个点被焊接。
图14.12中,点A和B在误差容限内应被焊接。我们"聪明地"焊接这两个点,计算A和B的平均值得到一个新的点D。现在C和D又在容限范围内被焊接,最终产生E。结果是点A和C被焊接了,它们本不在误差容限内的。并且,我们"聪明的"尝试也失败了,因为A、B和C被焊接,但结果并不是这三个点的平均。
这还不是最坏的情形,至少没有点跑出误差容限外去。但确实可以故意用更多顶点和不同顺序制造这种恶毒的例子,更不幸的是实践中确实存在这种问题,建模程序和自动生成程序常这么干。其实,即使不平均生成新坐标,依然会有上面的问题。例如不考虑平均坐标,以为应用一个简单的规则"总是将高序数顶点焊接到低序数顶点"就可以解决这个问题。
有一些防止出现上述问题的方法。比如,可以先找出所有误差容限内的顶点组,再焊接它们;或者不考虑已经焊接过的顶点;或者记录原顶点坐标,当顶点和它们相比在容限外时就不焊接。这些方法都过于复杂,我们不应为不显著的性能而增加复杂性。焊接是为了去除重复顶点,而不是为了网格消减:即大量减少三角形数,而尽量保持三角网外形不变。关于网格消减,必须使用更加高级的算法。
另外一个问题是关于三角网的附加信息的,如表面法向量、纹理映射坐标等。当点焊接时,先前的不连续消失了,图14.8就是一个例子。
最后,顶点焊接的直接实现非常慢。即使在当今的硬件条件下,数千个点和面的焊接也要用掉数秒钟。寻找焊接顶点对的算法是O(n2)复杂度的。一次焊接后的顶点索引替换需要遍历整个三角形列表;删除一个顶点也需要遍历三角形列表以修复比删除点序号高的顶点的索引。幸运的是,加以思考,我们可以找到一个快得多的算法。
5.2 面拆分
面拆分即复制顶点,使边不再被共用,它和焊接刚好相反。显然,面拆分会导致拓扑间断,因为面不再邻接。而这正是我们的目的,使得几何间断的地方拓扑也是间断的(如角和边)。图14.13显示了两个三角形的拆分,尽管我们把两个三角形分开以显示这里有多个边和顶点,这只是为了显示,顶点没有移动,新的顶点和边其实是重合的。
实践中,我们经常要拆分所有面。
5.3 边缩坍
边缩坍是将边缩减为顶点的方法,与之对应的是顶点拆分。如图14.14所示,注意到边缩坍使边的两个顶点变为一个,共享该边的三角形(图14.14中阴影部分)消失。边缩坍常用于网格消减,因为它减少了顶点和三角形数量。
5.4 网格消减
网格消减是将三角形和顶点数较多的网格变为三角形和顶点数相对较少的网格,并且要求网格外观和主要顶点尽可能保持不变。Hugues Hoppe指出zhi只用边缩坍就可以达到好的效果,选择要缩坍的边相对费时,视启发方法的复杂性而定。尽管选取缩坍对象的时间较长,但缩坍操作本身并不复杂。我们可将此过程离线记录下来,在实时需要时"重放"它,即可得到任意精细程序的风格。Hoppe的论文描述了如何利用顶点拆分来反演边缩坍过程,用此反演法生成的网格称为渐进式网格。
from : http://www.cppblog.com/lovedday/archive/2008/03/02/43554.html http://www.cppblog.com/lovedday/archive/2008/03/02/43557.html http://www.cppblog.com/lovedday/archive/2008/03/02/43565.html http://www.cppblog.com/lovedday/archive/2008/03/02/43570.html
|