Virtual Texture Tools & Practices

Virtual Texture Tools & Practices

前言:

  1. 别问能不能在移动端跑,每篇文章下面都有这种鬼畜评论,也是没谁了。
  2. 要批评技术实现请直接指出实现本身的问题,本人愿意友好交流,若能提出解决方案更是十分感谢,要是看到“这东西没经过项目验证没法落地”之类的老油条言论,那就别怪我一秒十喷,谢谢。
  3. 本人是纯粹兴趣使然的独立开发者(满头秀发),目前该管线不为任何以商业盈利为主要目 的的项目服务,同时源码都会第一时间开源 (本文用到的Demo比较简陋暂时不开源,也没什么参考意义,之后完整的实现方案会跟着地形系统一起出现的):
https://github.com/MaxwellGengYF/Unity-MPipelinegithub.com

最近看到Unreal Engine 4.23版本更新出了Virtual Texture这个功能,先前的文章中我们也讲过Virtual Texture可以完美支持基于四叉树的平坦大地形,但是我们对于Virtual Texture的使用也并没有展开详细讲解,本篇文章中我们将详细Virtual Texture到底是什么,怎么用,会给现有的渲染流程带来怎样的进步。

对于Virtual Texture,其真正的,或者说最基础的Virtual Texture的定义只有一个:一张可以接近于无限大(如512K × 512K)的,允许开发者手动控制部分加载(如每次只加载512 × 512 的区域)和卸载的动态贴图,也就是一张RenderTexture。从这个定义可以看出两个特性:

  1. 逻辑上是无限大的。
  2. 物理上并不会直接把内存撑爆,可以分块加载,只加载看得到的或者有用的。
  3. 完全实时,可以随时像普通的RenderTexture一样作为RenderTarget

因此在这3点之上就可以延伸出许多奇妙的用法,比如逻辑上无限大这一点,对一些渲染逻辑帮助非常大,比如传统的Mesh + Height map的地形做法就会涉及到一个比较麻烦的问题,那就是两个不同LOD级别的Mesh大小不一,中间的接缝基本没法消除,而这种问题对Virtual Texture来说迎刃而解,因为整个地形都在“同一张”贴图上,所以可以每一块地形都可以轻而易举的获取周围邻居的贴图材质信息,也不需要美术手动做各种低效的骚操作去掩盖了。再比如Unity一直没能解决的痛点:Lightmap streaming和GPU Instance Lightmap,在VT面前也可以瞬间得到解决,因为贴图无限大,所以可以肯定比整个游戏所有Lightmap加一起都要大好多倍,那么所有场景中每个场景每个物体只需要给出一个固定的,绝对的UV值,就都可以从“同一张”贴图中读取了,所以仅从这一点上来讲,VT的应用大大减少了美术无谓的工作量,提高了工具的生产效率,还顺便解决了许多曾经困扰的问题。

第二点和第一点是相对应的,逻辑上无限大,但物理内存可不是无限大,所以Virtual的用处就在此,这方面接下来就会讲到。

第三点,也是之前Siggraph上大厂们所看重的最重要的点,那就是完全动态的Render Texture,可以支持许多实时的高级特性,比如上一篇文章讲过的地形贴花,就是直接在地形的上方向下打一盏正交相机,像二向箔一样把这块地形所有的信息(甚至阴影)都降维打击拍平到贴图上,做到完全动态,完全实时的地形渲染。当然,既然是纯实时,自然有许多奇妙的操作可以发掘,比方说做昼夜变换?做粒子效果?具体用骚操作还要看具体需求,总之,自由度非常高,可以各种浪。

闲话说的有些多,这里直接开始上干货,从实现原理,基础用法和进阶应用三部分讲解Virtual Texture的使用和扩展。


一.实现原理:

顾名思义,Virtual Texture的实现原理一句话表达就是Virtual,像系统内存指针一样,当我们调用malloc方法时,操作系统会返回一个虚拟地址,这个虚拟地址会经过跳转表,跳转到物理内存上,具体为什么这么做,在大学的计算机组成课上想必教授已经讲得非常清楚了,这里就不再赘述,而Virtual Texture的实现思想与系统的内存分配原理几乎完全相同,那就是用一张储存ARGB64格式的贴图,这张贴图可以称之为Index Map, Indirect Map, UV Map等等,名字千奇百怪,但是作用只有一个:指向物理贴图的地址,或者叫UV,由于我们这里物理贴图使用的是TextureArray,所以实际上是UVW,其中R通道储存了分配的大小,GB通道储存了分配的位置偏移,A通道则储存了Array Element,因为DirectX最多支持2048个数组的元素,而16位固定位数的范围则是0-65535(归一化后是0-1),因此精度上绝对足够了。Ubisoft的分享中图示表达的非常清楚:

二.基础用法:

管线代码中的VirtualTexture.cs脚本单独负责了一整张贴图的所有管理,当然生命周期是手动控制的,是一个纯粹的struct,所以这里在游戏开始时初始化一下:

这一句话开出了一个3x3的Index Map,并且对象池内容量为9,正好与indexmap的像素数量相同,也就是说正好允许全部位置加载上,不考虑流式加载与卸载。调用LoadNewTextureChunks方法会加载从int2(0,0)到int2(3,3)中间所有位置的方块,并返回一个数组,这时候我们就拥有了9个TexArray中的元素的位置。

然后把序列化的数组中的所有值Blit到指定的位置上,这样就有了一张9个256x256的贴图的Atlas,最下方的Debug也如愿以偿的输出0,表示对象池内所有空间都已占满,没有浪费空间:


当然,也可以在这3x3的格子中塞一个元素,这样就是一张256x256的贴图了,而代码中的Debug也就会输出8,因为只开出了一个位置:

输出了右下角的图片,分辨率降为1/3

到这里,Virtual Texture的第一个好处就已经比较明显了,其逻辑大小不等于实际大小,同时内存的使用也比传统的Atlas科学许多,然而Virtual的特性在这两个例子中表现的并不全面,Virtual的另一个好处就是可以直接改变映射位置,比如使用下方的代码,可以直接对图片进行快速降采样,把第一次演示的图片的分辨率从3x3降低到1x1,并铺满整张贴图,输出的对象池中剩余的值也就有8个了,这招在需要快速降低LOD时非常好用:

比起第一张贴图,分辨率有明显降低,直接降低3倍

至于说完全动态这个特性,这里做一个简单粗暴的实验:动态合并贴图和材质,比如建两个方块材质,一个给512x512贴图,一个给1024x1024贴图:

轻松合并:

材质下并不需要单独挂载,只需要给出一个偏移和大小就可以:


三.进阶应用:

很显然,造出这么庞大的一个东西出来只用来做合并模型合并材质或者做GPU Instance之类的低端工作,似乎有些太无聊了。在应用方面,Ubisoft分享的Procedural Virtual Texture是一个非常好的实现方案,这套方案完美的贯彻了VT的动态分配,动态更新的特性,在实时运行时分帧的将粗粒度的贴图,细粒度的贴图,抠缝增加细节用的贴图以及点缀效果用的贴花(小石子,青苔,斑马线等)分类到不同的Detail Levels中,并且在切换LOD决定接下来将要加载,渲染的层级,实时的分帧的推到贴图上。这么做的优点是远多于缺点的。因为众所周知,在渲染大地形时如果把极其庞大的地形贴图(如512K×512K)直接打包进硬盘,那么游戏体积将会非常恐怖,假设不考虑PBR材质和高度信息,只考虑一张Albedo和一张Normal,那么单纯一个地形的贴图也需要2T的空间。。这很明显是不现实的,即使我们制作的游戏没有那么大的地形,庞大的包体也同样会损害玩家的入门体验,劝退许多新玩家,因此把一部分贴图的运算放到实时,解决离线信息过量的任务,就成了当务之急。

实时的计算混合,烘焙贴花。

最新一代的《Assassin's Creed Odyssey》中表现出的地形贴花质量和数量更高一筹,比如这里地面的石子,积水,土坑等等,基本都是平面的贴花(尸体不是,尸体是被我砍的,哈哈):

我们这就用一套相对简单一些的工具,实现一下这种根据LOD实时加载的技术,这里贴花的基本实现原理比较明确,就是一个单独的渲染通道,或者说相当于一个单独的摄像机类型,只对某个层进行渲染,并把渲染结果叠加到VT上,因此我们需要往渲染管线中插入逻辑。好在MPipeline Framework在开发时就已经想到这种特殊的插入需求,所以开了一个Interface的提交让渲染对场景内的脚本完全公开透明,虽然“在MonoBehaviour中直接编写一个渲染管线”这个做法听起来玄幻不靠谱,但是实际上这对于整个架构并没有太大影响,因为SRP已经把提交抽象成了CommandBuffer和ScriptableRenderingContext这两个类型,再配合Camera组件提供的渲染参数,因此可以说只要有这三个就可以随意令客户端程序定制渲染(这里为了消灭GC,还特地把Interface转换成指针进行储存,因此使用的时候要注意让传入的实例保持被引用,防止GC扫描托管资源的时候翻车):

接下来正常在脚本里写一个自定义的微型渲染管道,绘制摄像机区域内所有贴花Shader:

到这里时,基本给地形做实时渲染绘制的准备工作就已经万事俱备了,接下来是加载方面,我们将使用Unity最新推出的Addressable进行资源管理,经过测试这套新系统的封装对美术和策划非常友好,拖一拖就能引用Editor目录下的资源,而且异步加载也比较稳健,性能方面目前没发现什么需要担心的问题。

当加载一块地形时,先从Virtual Texture中分出对应的一块Array Element,使用从硬盘加载固定的贴图组,并进行混合,混合完毕后再调用摄像机绘制贴花,使贴花覆盖在贴图之上,贴花可以是纯平面+贴图,也可以是一些小模型,比如上方游戏中的石子土块等,经过“二向箔”打击,降维到Virtual Texture上,到此就完成了一块地形的绘制。毫无疑问,在地形中的Virtual Texture是使用四叉分割和合并的方式进行LOD的切换,四叉分割的思路基本相当于逆MipMap的思路,也就是一个金字塔形,这样我们把这个金字塔降维成普通的二进制文件,就可以正常读写了。当然,同样为了不找C#的托管堆的麻烦,我们手动分配一个固定大小的字节数组储存GUID,而不是使用string,并把这个结构体封装成FileGUID类型,每一块地形目前设定是4套PBR贴图混合,其实4张绝对是够的,传统的地形系统(如UE4 Landscape)主要依赖于实时读取并混合多张贴图,因此主要靠贴图的混合增加地形复杂度,然而我们这里贴图混合只是一个地基,主要的细节表现是依赖贴花的,因此如果项目性能优化有需求,这个数量还可以进一步砍成3张甚至2张,这就是Virtual Texture与古典的地形系统比起来最根本的优势。

每块地形的数据结构(总共13张图,每套PBR需要3张贴图,总共4套,外加一张叠加用的Mask Map):

这样可想而知,每次MipLevel+1,Chunk会分4倍,如果我们有3层Detail Level,那么文件中将会存1 + 4 + 16 = 21个VirtualTextureChunk,直接使用.Net FileStream提供的流式二进制加载,可以非常轻松的从硬盘里直接读取,美术修改完地形信息后也好直接修改硬盘,这会令后期的工具链搭建非常方便,怎么使用FileStream加载卸载的代码这里就不详细展开了,并没有太大的技术难度。

在有了GUID索引的存储和读取之后,就可以使用GUID实例化AssetReference,也就是Addressable中的句柄,并加载贴图到内存中:

全部加载完毕后先从Virtual Texture中开辟出指定的区域,然后在Compute Shader中使用Mask混合:

我们这里测试用了3级LOD,也就是最大16 × 16,最小1 × 1的这么个地图,我们先从Virtual Texture中开一块16 × 16的大图,作为最低一级的LOD,加载后混合,这就相当于一张1024分辨率的贴图铺平在16 × 16的格子上(贴图是随便找的,所以有些难看,请见谅):

但设想这是一块几百米的地形,那么这个精度肯定远远不够,因此当玩家靠近时我们需要加载LOD1,这里故意用了和LOD0截然不同的贴图,来表现加载效果,实际开发中需要使用与上一级LOD相同的贴图,并调整TileOffset保证地形看不出缝隙,然而本文我们着重于讲解Virtual Texture,因此关于地形的工具链暂不在讨论范围内:

经过细分后,分辨率从1024直接提升到了2048,这时可以说就完成了下一级LOD的进步,我们还可以再细分一级LOD,原理和上一级一样(只细分了一角):

在完成混合后就可以开始绘制贴花了,刚才只是写了绘制贴花用的渲染管线部分,并没有连Shader一起写出来,Shader的写法多种多样,既可以是用Alpha Blend直接投影的单个平面,也可以正常绘制正常ZTest的模型(这也是为什么上方代码要开启16位的Depth Buffer),属于一个微型的Deferred Pass,这里先写一个最基础的Alpha Blend Shader,值得注意的是,这里输出的法线是直接投射的世界坐标法线,如果这时投射的不是一个平面而是一个其他的物体,那么就要特别注意将法线转换的Tangent Space:

正交摄像机的摆放也没什么特殊技巧,既然VT本身是横平竖直的分块,那么直接代码自动瞄准到对应的Chunk位置就可以了:

在场景内像摆模型一样正常摆贴花,然后手动提交给脚本一个渲染指令即可完成:

这个效果就基本达到我们想要的感觉了,至此我们的进阶应用部分也到此为止,非常抱歉由于本人开发时间较为紧张,没准备好足够的美术资源,且地形系统还未开发完毕,因此演示比较难看,希望各位看官见谅。


结语:

在本篇文章中我们集中的讲解了Virtual Texture的原理和一系列实现,然而单纯的一个组件能做到的工作比较少,贴花和动态混合这种高级效果也需要配合新的地形系统来使用。而MPipeline的新地形系统目前还在紧锣密鼓的研发中,我们将会逐步兼容并完善Virtual Texture,并逐渐完成对Houdini Engine的支持和互动,尽快定制一套面向高端平台的完善的大地形渲染实现。

编辑于 2019-09-29

文章被以下专栏收录