引入
地形是3D游戏中不可或缺的基础部分,特别是对于要表现室外场景的游戏来说。随着图形硬件的发展,地形渲染技术也经历了若干变迁。主要包括几个方面:LOD,光照和阴影,材质表现。由于本人的时间精力有限不可能面面俱到,所以只能是浅谈辄止,粗浅的叙述一下相关的技术和一些思考,欢迎交流指正。
关于 LOD
早些年,由于图形硬件的瓶颈在于顶点和三角形处理上,因此发展出一批目的在于尽可能减少顶点和三角形数量的算法,如ROAM。后来随着显卡性能的提升,渲染效率的瓶颈逐渐变成了总线上的数据传输速率,地形渲染随之演变成尽可能将预计算好的数据存储于显卡上,从而避免每帧传递数据,如Geo-Mipmapping。还有后续的算法在此基础上继续改进,如Chunked LOD。
除此之外,地形LOD算法还需要处理的问题包括如何消除LOD级别改变时发生的视觉上的突变。一种常见的方法就是Morphing。不过不知是出于效率还是工程上的考虑,Crysis的地形上并没有做morphing。
再后来,随着Shader Model 3.0中增加了Vertex Texture访问以及对Instancing的支持,地形渲染技术则有了更多的选择,例如通过VertexTexture技术,可以在VertexShader中算顶点,而Instancing技术更进一步减少了数据传输量,进一步简化了CPU端的计算和数据传输。
参考:CDLOD,这个作者正在基于DX11实现他的下一个版本的CDLOD,将会包含下面要说的Tessellation技术。
到了DX11,新引入的Tessellation技术则让引擎开发者拥有了将整个地形渲染完全搬到显卡上去做的能力。如果说SM3.0中我们只是在硬件上动态生成所需的地形顶点,那么到了DX11中,我们就可以在硬件上实现LOD了,我们可以GPU上决定每个Patch应该有多细密的网格,并且在GPU上处理好和邻接Patch的接缝问题。关于这个话题可以参考GPU Pro2中的Terrain And Ocean Rending with Hardware Tessellation。
这意味着到DX11普及的时候,以往的那些在用于CPU端计算LOD索引的很多算法,都不再被需要了。当然,上述算法中产生的核心思想仍然会继续起作用——例如利用四叉树做地形的数据管理,例如如何在LOD改变时避免视觉上看到跳变等等。
p.s.有时候难免对于国内的Win7普及率感到无奈……
关于光照和阴影
由于地形是一种静态的模型,不会移动,相对光源的位置是不变的,因此实时渲染地形的时候,很流行的方法是采用LightMap对地形光影做预计算,而传统意义上的光照计算,则往往是在vs阶段(这里指顶点光照,若是逐像素光照,则是在ps阶段)实时进行。
由于LightMap省却了实时进行光照计算的计算量,在效率上有所提升,并且一些对计算量要求较高的技术如全局光照,如果采用实时计算会导致性能急剧下降,因此在实际中往往也是预计算并存储于LightMap中。
另一方面,LightMap不便存储动态的光照信息,当光源的角度,颜色发生改变,LightMap就需要随之更新。而这在实时游戏中往往很难做到。
目前主流的实时阴影技术基本都是基于ShadowMap的,在Doom3中使用的Shadow Volume,现在已经很少看到了。
ShadowMap的基本原理是从光源的位置和朝向绘制一张场景的深度图,成为ShadowMap,然后再在实际渲染场景的时候,将每个像素的位置到光源的距离和ShadowMap中的深度值进行比较,如果发现该像素的深度小于ShadowMap中对应位置像素的深度,则不在阴影中,反之,则在阴影中。
ShadowMap的好处在于可以方便的实现很多个物体的阴影,物体的自阴影。
但是也有几个问题:
1. 边缘会出现难看的锯齿状走样,一方面因为像素的含义是深度,因此相邻像素的值是跳变的,可能一个值为7,邻接的就是200,你无法对深度做过滤;另一方面和ShadowMap的大小有关,同一个场景,使用的ShadowMap的Size越小,越会出现较大的锯齿。
2. 如上所述,由于存储的是深度,因此ShadowMap难以判断一个像素是否处在阴影边缘中,难以给出柔和的半影过度
3. ShadowMap只适合于平行光和SpotLight,对于点光源需要计算多达6张ShadowMap(利用DPSM可以减少到2张)
为了解决ShadowMap局限,有很多方法被发明出来,例如,采用PCF获取软边缘,不过PCF需要多次自行采样纹理,效率上有损失,另一方面PCF不是真的计算软阴影,而是通过采样的手段去找出边缘,所以无法根据距离远近产生真实的半影过度。
还有种方法是VSM(方差阴影贴图),其原理是通过车比雪夫不等式(Chebyshev)去计算一个像素在阴影中的概率:
 |
| 公式来自于Variance Shadow Maps by William Donnelly_ Andrew Lauritzen† |
具体来说,它在渲染深度的时候同时存储深度和深度的平方(不同的颜色通道中),然后对ShadowMap做过滤,这样得到的就逐像素的深度的期望μ和深度平方的期望,利用这两个值可以求得方差σ,然后再用车比雪夫不等式计算概率。(具体原理及实现可参考 vsm)
关于材质表现
传统的地形表现比较常见的是用多层纹理混合,也称为Texture splatting。
 |
| 图片来自Wikipedia |
这种做法可以实现多种贴图之间的混合过度。优点是实现简单,而且贴图通过反复Tiling即可构成整个地形表面,从而节省了纹理资源。
缺点则是,如果美术稍微偷点懒,就可以看到大片大片的Tiling,重复感非常明显。以魔兽世界为代表的一大批游戏引擎用的都是Texture splatting来表现地表纹理。
在Crysis中有一个在表现效果上和效率上结合得非常完美的材质系统,Crysis为了给地表提供尽可能丰富的细节,支持给顶点赋材质,这样就能够支持地表的Bump Mapping以及更高级的POM。而传统的Texture splatting在混合时不去区分实际的材质,是不可能做到这一点的。当离远之后,Crysis就不再渲染细节纹理的pass,而是只渲染地表颜色图。
来一张Crysis的炫技图(图片来自互联网):
 |
| crysis |
接下来,为了引出Virtual Texturing,先介绍下Clipmap。
在类似GoogleEarth之类的地图软件中可能使用了一种类似于Clipmap的技术,这种技术基于mipmap,同时又对之作了扩展。
传统的Mipmap是从一张贴图原始的Size开始直到Size为1有一个完整的Mipmap链,比如一张1024×1024的贴图,会有从512×512直到1×1的全部的mipmap版本,同时存在于显卡中,到了纹理采样的时候根据mip级别采样不同的纹理。
 |
| 图片来自The Clipmap: A Virtual Mipmap |
而Clipmap中的贴图可能很大,比如64000×64000,这样就不可能把它和它的完整的mipmap链放到显卡里去了,所以他只放最低级别的那些Mipmap到显卡里,超出显卡能力的部分也放,但是只放一部分(需要的),就好象是Clip过了一样,所以叫Clipmap (= =)如图:
 |
| clipmap |
虽然我们无法把整个贴图完全加载到显卡上,但是我们观察到足够清晰细节的距离不可能太远,离远了就切到下一个mip级别,所以随着到视点距离的增加,从最高精度到最低精度有一个逐渐的变化过程。所以这种做法是可行的。
很多人把Clipmap和Virtual Texture搞混了。其实这是两个完全不同的技术。虽然在对Mipmap的使用方面有类似的地方。
我觉得未来的趋势应该是把地形和模型的贴图都统一纳入到Virtual Texture中管理,这个技术是由卡马克在id tech 5中引入的。在Id Tech5中,通过Virtual Texture技术可以制作没有任何重复感的地表(Virtual Texture也支持模型贴图),从此再也不愁Tiling的问题——只不过可能就得苦了美术和编辑器制作人员了,而且Virtual Texture技术存在许多在制作流程上需要克服的问题。
Virtual Texture说白了就是一张巨大无比的大纹理,可能有1024000 * 1024000这么大,存储在磁盘上。但是到游戏中,只加载实际需要的就可以——实际需要一方面是指mip level上的需要,远处的只需要低精度的纹理,近处才需要高精度,另一方面是指不需要加载那些不进入可视范围内的纹理(看不到的)。
Virtual Texture技术实际上借鉴了计算机中的Virtual Memory的理念。我们知道早期的计算机物理内存没有4GB,但是应用程序却可以通过32位指针访问到整个4GB的地址空间,好像这么大的内存都可以访问一样,但是实际的物理内存可能很小,只有512MB,这是怎么做到的呢?
操作系统把内存按页进行管理,每个页4KB大,并且只把应用程序需要访问的内存页放到到实际的物理内存中,如果应用程序访问一块内存,而这块内存又不在物理内存中存在,那么应用程序会暂停执行,操作系统执行换入操作,并把应用程序需要的内存页拿到物理内存里,然后再把执行权交回给应用程序,当然这一切对于应用程序的开发者来说都是透明的。
虚拟纹理(Virtual Texture)也是采用类似的方案,实际的Physical Texture(想像成对应于物理内存的实际纹理)可能只有4096 * 4096这么大(根据实际显卡的支持程度决定),但是3D游戏中各物体的uv坐标则可以寻址远超出这个物理纹理范围的大纹理,因此virtual texture也需要把物理纹理分页,比如切成256×256的小块纹理,并且根据实际需要进行换入换出操作。这里一共存在从硬盘到内存,再从内存到显卡的三级缓存,这个过程中有许多复杂的地方要处理,比如怎么知道应该加载什么mip级别的贴图,怎么知道应该加载那块贴图等等。基本的思路是通过预先渲染一个pass,将需要加载的mip级别和具体的页面索引算出来。
Virtual Texture目前网上已经有了开源的实现,感兴趣的朋友可以参考下 Sparse Virtual Texture,这里,还有id Tech5的介绍,CryEngine对Virtual Texture的看法。
本文中提到的技术,在实际的实现中都要解决各种实现上或者工程上的问题,不可能如我在这里夸夸其谈这样简单,所以本文只能是简述下基本思想,算是对近期一段时间的学习做个整理。
参考
- 关于地形渲染技术的杂谈
|