今天的分享主要包括以下两个方面:第一,大世界多层纹理混合,这方面我们主要介绍一下如何突破移动端的限制来实现多层纹理混合;第二,我们将介绍一下如何修改UE4渲染管线来实现地形与模型之间交界处的柔和过渡。
首先我先说明一下我们使用的UE4版本是4.21的版本,当然在原生引擎的基础上,我们做了大量的修改来适应我们项目的需求,包括接下来我们要说的地形渲染技术。
那说到地形渲染技术,其实它是一个很宽泛的话题,它包括了一些地形网格的生成、地形光照、接缝处理、纹理混合等方面。
那我们来谈一下,地形的纹理混合。首先我们来介绍一下一些传统的纹理混合方式。最常用的一种叫做基于权重的混合,也称为Texture Splating。它的基本原理就是使用一张叫做Splating Control的贴图,来控制最多4层的纹理进行混合,它的优点是能实现一种,平滑的过渡,但是缺点是不太自然。这有一张示意图,我们可以看到在左边是一种沙砾,而在右边是是砖块,在砖块和沙地之间能实现一种较为平滑的过渡,但是从实际的生活当中这并不符合实际情况,因为我们可以看到实际生活中沙粒是更多地掺杂在砖块的缝隙之间的。
(混合示意图)
接下来一种改进方式就是基于高度的混合,这种高度的混合的方式,就是在每张纹理的Alpha通道里存储一下高度的信息,这样我们通过比较每张贴图的高度差来决定我们该显示哪一部分,比如说在砖块的这些缝隙之间的Alpha值就会低于在沙地的Alpha值,这样我们就能实现一种基于高度的混合,在右边那张图显示的是混合后的结果。我们可以看到这些沙粒就会很自然的分布在这些砖块的这些缝隙当中。
而UE4提供了原生的一些地形混合节点,也就能够实现我们刚才提到的这两种混合方式,在材质编辑器里有个叫做Layer Blend的节点,在它的属性上我们可以选择Blend Type 为Weight Blend或者是Height Blend,这两个分别能实现刚才所提到的权重混合以及高度混合的方式。
在对于《ob电竞》的项目而言,我们并没有采用这样的方式,为什么呢?我们还是首先说一下《ob电竞》的项目背景。《ob电竞》的项目是基于一个8K的无缝大世界,它分为了16×16个地块,然后每个地块又包含了4个Landscape Component。
其中每个Component都对应着不同的一种地形地貌,包含比如说常见的雪地、沙地、草地、湿地等。这有一张项目的项目截图,我们可以看到在远处就是大片的雪山或者雪地,然后在中间有一层沙漠的地形,在靠近近处是一片草地还有一些道路、纹理。
在这样的情况下,我们就会使用大量的纹理贴图,而最关键的一点是在移动端有一个对采样有一个限制。如果我没记错的话,在OpenGL 3.1上最多支持16张纹理限制,这就极大地限制了我们项目在地形上的纹理混合方式。
那如何突破这种纹理采样的限制?答案就是使用Texture2DArray这种技术。那我先简单说一下什么叫Texture2DArray。简单来说,Texture2DArray就是一组具有相同2D纹理属性的一个数组。
我们看到在右边有4张的这种纹理,它们的属性,比如说它的宽、高以及它的采样格式,它的纹理采样坐标是三维的,就是在传统的U,V方向上在加一层W,这个W表示的就是其中的纹理索引。
我们可以看到在右边有一个示意图,这里面包含了4张纹理,然后我们使用个(U,V,W)三维纹理坐标,去采样Texture2DArray数组。
使用Texture2DArray有什么好处?它带来的最大好处就是,能够节省我们纹理Binding的消耗。
我们知道在CPU端纹理Binding是一个非常耗时的操作,比如我们如果要采样8张单独的纹理,那我们需要对这8张纹理分别进行binding,而如果我们使用Texture2DArray的话,把这8张纹理放到Texture2DArray里去,就只用binding一次,这极大减少了CPU的消耗;第二个好处是Texture2DArray在硬件层面支持层与层之间的过渡,比如说我们的W值我们可以取一个小数,比如说取1.2,这样的话我们就在第一层和第二层之间,在硬件层面可以做一个混合。
但是使用Texture2DArray也有一些限制,比如说第一点对于UE4来说,在4.25以前的版本是需要手动添加Texture2DArray的支持的,由于刚才我们提到了我们项目使用的是4.21的版本,所以说我们这一块我们做了大量的工作,包括添加编辑器方面的支持以及Texture2DArray 、MipMap、ASTC压缩格式以及它在安卓及 iOS上的一些底层的接口的支持。
第二个关键点就是Texture2DArray里面的每张纹理属性要保持一致,最关键一点就是它的长、宽这些属性,还有它的压缩格式等,这我们可以在编辑上做一些校验,在美术添加相应的资源的时候对每张贴图的属性进行校验,如果说发现有不一致的地方给出一定的提示,来保证这个Texture2DArray的贴图属性的一致性。
第三个就是就是说Texture2DArray需要gles3.0以上的支持,不过好的方面是从我们目前项目经验来看,市面上大部分的机型都能支持Texture2DArray。
刚说完Texture2DArray,我们接下来说一下,我们如何使用Texture2DArray来实现多层纹理混合。
首先我们会维护两个全局Texture2DArray的资源,第一个主要是存放BaseColor的;第二个是主要存放NormalMap,然后对于每个地块来说,我们都会维护一张索引图,以及一张对应的权重图。我们这种索引图的大小就和我们的地形的分辨率是一样的,都是256×256,而我们的权重图呢是1024×1024的,这可以看到是我们的索引图4倍大小。那为什么4倍大小?待会我们再会提到。
这是我们的基本的一个流程,可以看到首先采样一下这个索引贴图,然后从索引贴图的采样得到这个索引和地形的纹理UV组合成一个三维的纹理采样坐标去采样这张,BaseColor这个Texture2DArray的这个数组,然后再去采样我们的对应的Weight Map,从Weight Map采样出来的值再和BaseColor取样出来的值进行混合。
那使用这个方式当然也会带来一定的问题,这我们一个截图可以看到,在这张图上地块有一些断层,也就是一些马赛克的效果。
那为什么会产生这种这种问题呢?
这有一个简单的分析,左边的这个示意图表示两个相邻的地格索引,可以看到在左边的这个地形地格索引里,它的RGB通道分别存储了索引为2、5、7的这几张贴图,而它们的权重对应为0.7、0.2、0.1,而右边的索引格存储是3、2、5号索引的贴图,它们对应的权重分别为0.8、0.1以及0.1。
在理想的情况下,比如说拿2号贴图来说,我们应该对二号贴图进行一个从0.1~0.7的左右的线性差值,对于5号贴图我们应该做0.2~0.1的线性差值,但是实际情况下,由于硬件是按照通道来进行差值的,我们实际的情况就是0.7~0.8的一个差值过程,这就造成了一个边界的断层。
那我们的解决方案是什么呢?就是刚才提到的把索引图扩大4倍,把权重图分别扩大4倍,然后通过自定义UV采样,让这些点精确地分布在这个采样边界上。
上面有一个示意图就是每个地格,这是原始的分辨率,然后我们把它扩大到4X4的一个采样范围,可以类似于超采样的一种机制,在这个过程当中我们需要保证,每个方格里的内权重被插值成平滑的过渡;第二点是如果是在邻居,不存在某一层的索引,我们需要在边界采样,保证为采样值为0,否则的话这一层就会断层消失,这就是我们使用对应的这个解决方案以后达到的效果。
在右边的图上我们就可以明显的把这些马赛克或者断层给解决了。
(最终解决方案对比图)
刚才这种方案上,我们还对应地在移动平台上做了一些相应的优化,比如说在法线贴图里我们的B通道,存储了一些Roughness的信息,这样可以节省一部分纹理的开销;第二个我们使用的是QualitySwitch节点,来适配不同机型的档位;对于一些低端的机型我们只保留一些最基本的效果;而第三种优化方式是动态的替换Texture2DArray这种纹理,因为Texture2DArray它毕竟还是会带来一定的副作用,也就是说它对于内存带宽以及纹理有一定的开销,我们可以在不同的索引块,替换不同的这种纹理索引,在运行时的话这样就可以节省一下内存的一些开销。
剩下的一些其他相关的技术,比如说我们可以使用DetailNormal,就是一些细节法线来增强地形的细节的表现;第二个我们是用在Custom UV节点里进行计算,这样可以避免一些移动平台上,因为精度不够而出现一些地形马赛克的情况。
以上就是我们刚才提到了一些,在地形混合方面的技术。
接下来我们来谈一下,模型与地形之间的柔和过渡,我想这也是大部分项目会遇到一个问题。
有一张截图,我们可以看到在红框里这个石头和下面的沙地的交界处,有明显的很生硬的边界,这也是为什么我们会采用一个新的技术的原因,来解决这个瑕疵。
首先我们还是说一下,一些传统的解决方案。
第1种方案:在模型边缘的地方使用一些半透明的材质,然后让这些半透明的材质部分嵌入到地形里,就是能够实现一些半透明的过渡效果,不过这种方式也有很多缺点,主要就是需要对模型制作一些半透明的材质,如果说我们的地形上模型非常多的话,工作量会非常大;还有一个比较致命的缺点就是这些模型如果一旦放入了地形以后,就不能随意地挪动了。
第2种方案:就是Pixel Depth Offset+DitherTemporalAA的方式,这个方式在网上有很多案例教程,我们也尝试过类似的方法,但是效果不是很好,同时移动端大部分机型支持的并不是太好。
第3种方案:Runtime Virtual Texture,这是一种比较新的技术,这大概是在4.23以后的UE版本才能够支持Runtime Virtual Texture,并且在移动端还不太支持,所以说我们并没有采用以上这种方式。
那我们使用的方法是,用一句话来概括就是在MobileBasePass的阶段新增了一个渲染队列,把这个队列放在了Mask渲染之后,同时也在Translucent之前。因为我们的地形其实它使用的Render model其实还是属于Opaque的。
在这种情况下,我们需要让地形的渲染顺序延后到普通的Opaque和Mask渲染之后,由于UE4引擎只提供了几种固定的Blend model,所以说我们需要增加一种特殊的渲染队列来渲染地形。
那么这个队列要放在哪里?刚才提到了需要放在Opaque和Mask之后,同时它是基于场景深度的一种混合方式,所以说我们也要同时放在Translucent渲染之前。
接下来有一个示意图:
我们可以看到左边就是UE4原生的一个渲染管线,在MobileBasePass阶段它首先绘制了Opaque的这种物体,然后绘制了Mask的物体,接着在MobileBasePass渲染完以后,再渲染Translucent的这个物体。
经过我们的修改以后,刚才提到了我们是在MobileBasePass阶段,新增了一个渲染队列 这个队列是在Mask之后,可以看到在左边,Mask后就是我们新增的这个渲染队列的位置了,那增加了新的队列以后,我们同时需要在Shder里进行一个基于深度的混合。
下面给出了一段伪码是表示我们基本的混合方式:
其实也很简单就是通过获取到场景的SceneDepth,还有当前正在渲染像素的PixelDepth,我们首先做一个差,然后根据这个差值,我们来混合一下SceneColor以及PixelColor来得到最终的渲染结果。
这里我们可以看到,这就是我们经过修改混合以后的一个效果,左边图是在修改之前。
(修改混合效果图)
你可以看到,在这个Opaque的这个石头以及地形的交界处,同样是一个很生硬的边界,而经过我们的修改以后,在交界处就出现了对应的过渡的范围,一个基于深度的过渡的范围,它可以比较自然地进行一个混合,而刚才说到我们把这个特殊的队列放在了Translucent之前,如果我们想对一些Translucent的物体也进行能达到这种在交界处混合的效果,地形的这种和模型的交界的混合,如果说我们想对一些Translucent的物体也实现类似的效果。
这里我们也可以做到的,方法其实也和刚才差不多,但是唯一的一个点就是我们不需要把这个Translucent的物体放到刚才新增的那个特殊的队列里,我们直接就在Shader里实现了这种基于深度的混合。
为什么不需要把Translucent放在前面,因为Translucent原本的渲染完就是在MobileBasePass后,也就是在Opaque和Mask渲染完了之后,它本身也就比它们渲染晚一点,所以说我们就保持原有的Translucent渲染的这个队列,同样在Shader里直接就实现一种基于深度的混合。
这里有一个示意图:
我们可以看到上面有一个半透的面片,它是嵌入到这个地形里面去的,而在我们修改之前,这个半透的面片和地形的交界处也是一条很生硬的边,看起来非常不自然,而我们通过刚才的的混合方式以后,你可以在半透明的物体和地形之间,出现一个比较柔和的过渡。
当然,我们为了方便给美术修改,我们在边界线上也添加了对应的开关,就是对编辑器的一个修改。
红框的地方上面就是一个滑动条,它可以控制我们这个过渡的范围,我们限制到0~1之间,而下面这个开关选项表示我们是否开启地形和模型之间的混合这个功能。
当然使用这种方式的时候我们有一些注意事项:
第1点:就是说在Shader中我们进行深度的混合的时候,为了达到一种平滑过渡,我需要使用深度的倒数,因为在ZBuffer当中深度本身不是线性变化的,而深度倒数才是线性变化,所以说我们需要用深度倒数来做这个差值。
第2点:我们可以在移动端使用FrameBufferFetch这个接口来避免生成一个额外的RT从而达到节省一种性能。当然这种主要使用FrameBufferFetch接口的话,需要硬件支持扩展,就是GL_EXT_shader_framebuffer_fetch的扩展。目前从我们的测试经验来看,大部分机型是能够支持这个扩展的,就是市面上的主流的机型。
最后我们来总结一下今天刚才提到的这两个点:
第1点:就是多层的纹理混合。我们使用的是Texture2DArray这种技术,来突破移动端的限制。
第2点:就是主要用超采样的索引贴图,同时使用自定义的UV来避免断层,而对于模型与地形的过渡,我们是通过扩展UE4的渲染管线在MobileBasePass阶段,Mask的渲染后面我们添加了一个新的渲染队列,同时在Shader当中我们实现了一种基于深度的混合。
我们的今天分享就到此。
如若转载,请注明出处:http://www.ashkeling.com/2020/12/405907