上一篇《浅谈Virtual Texture》主要是对理论知识的介绍,本篇开始对Unreal Virtual Texture的源码做一个导读。
内容包括Virtual Texture的流程和一些技术实现细节,默认你已经对Virtual Texture有一定的认识,如对技术概念有疑惑,可以先看上篇。本文会先从整体出发,介绍Unreal实现的大概内容和流程,以及结构关系,然后再深入到细节,尽量还原Unreal的设计。
一、Unreal 实现的内容
首先,先给Unreal的Virtual Texture的实现给一个大体上的介绍。Unreal是基于Software Virtual Texture,并未涉及Hardware的内容,实现了Procedural Virtual Texture,Unreal叫Runtime Virtual Texture,并未实现Adaptive Procedural Texture。地址映射使用了indirection texture的page和MIP level的映射方式。Texture Filtering方面实现了Bi-linear Filtering、Anisotropic Filtering和Tri-linear Filtering,Bi-linear Filtering是基于border来实现的,Tri-linear Filtering则是利用TAA的一个实现,这个实现是其特有的,Anisotropic Filtering则是自己计算AnisoBias来实现的。
Feedback Rendering是跟GBuffer同时的,也就是结果会延迟一帧,分辨率可以用VIRTUAL_TEXTURE_FEEDBACK_FACTOR来控制,是UAV来操作的。Transcode方面使用的是Crunch,也就是压缩方式是DXT,由于Unreal的磁盘上的纹理是uasset,所以没有其他通用image格式的压缩了。
二、Unreal Virtual Texture流程
Unreal Virtual Texture的基本流程跟Software Virtual Texture是一致的,主要的逻辑在FVirtualTextureSystem的Update函数里。
可以看到,FeedBackAnalysisTask和GatherRequestsTask是多线程做的。虽然是多线程,这边是同步等待执行的,因为后面的执行依赖前面的结果。在这些流程中,SubmitRequests这个流程会比较复杂,我从中再抽取出与Physical Texture加载相关的流程,Steaming Virtual Texture的加载过程:
可以看到,这里包含了Streaming Load的部分和Transcode的部分。Runtime Virtual Texture就比较简单了,因为是实时生成的:
包含了渲染Mesh,压缩和重新编码纹理的过程。这里纠正官方视频中的一个错误,视频中说Runtime Virtual Texture只会渲染一次,这是不对的,它会实时渲染不存在Physical Texture中的Tile,为什么移动物体不会使得Physical Texture更新,是因为Tile已经存在于Physical Texture上了,只有当当前的Tile被替换出去,才会发生再次渲染更新Tile。
三、Unreal Virtual Texture 结构
Unreal设计了一个非常漂亮的结构,使得整个系统能够优雅的合作运行。
从数据结构方面主要分为两大块,就是上图的左上方部分和右上方部分。FVirtualTextureSpace用来管理Virtual Texture部分,FVirtualTexturePhysicalSpace用来管理Physical Texture部分,中间的FVirtualTextureSystem则负责具体的行为逻辑,串联起两边。左下角的Producer部分是负责制造Physical Texture Tiles的,右下角的IVirtualTextureFinalizer部分则是负责将Tiles “拷贝”到Physical Texture的确定位置上。
如果只是想大概了解一下Unreal的实现,到这里就可以结束了,后文会是比较琐碎的实现细节。
四、FeedBack Rendering
Unreal的FeedBack Rendering的实现是和BasePass渲染同时的,使用一个RWBuffer来实现不同分辨率的输出,VIRTUAL_TEXTURE_FEEDBACK_FACTOR参数来调整分辨率。具体代码在VirtualTextureCommon.ush的FinalizeVirtualTextureFeedback中,这个在每个需要生成Feedback buffer的pixel shader的末尾调用。FinalizeVirtualTextureFeedback需要一个FVirtualTextureFeedbackParams.Request,这个需要找一个Material,然后看它生成的HLSL,会找到是如下方法得到的:
VTPageTableResult Local1 = TextureLoadVirtualPageTableBias(VIRTUALTEXTURE_PAGETABLE_0, VTPageTableUniform_Unpack(Material.VTPackedPageTableUniform[0*2], Material.VTPackedPageTableUniform[0*2+1]), Parameters.SvPosition.xy, Parameters.VirtualTextureFeedback, 0 + LIGHTMAP_VT_ENABLED, Parameters.TexCoords[0].xy, VTADDRESSMODE_WRAP, VTADDRESSMODE_WRAP, View.MaterialTextureMipBias);
数据编码是放在了一个32bit uint里了,|4bit pageid|4bit level|12bit pagex|12bit pagey|。对于半透明物体和单像素多Page的情况,是使用了一个跟pixelpos、depth、FrameNumber相关的随机值来解决的:
const float AlphaThreshold = frac( PseudoRandom(PixelPos) + // Random value in 0-1 on 128 x 128 pixel grid
SvPosition.w + // Add in depth so we pick different thresholds on different depths
(FrameNumber / (float)VIRTUAL_TEXTURE_FEEDBACK_FACTOR) // Add in framenumber for extra jitter so the pseudorandom pattern changes over time
);
上一篇文章提到过,这种方案理论会出现同一个Pixel引起反复加载的情况。
五、FVirtualTextureSpace
FVirtualTextureSpace代表的是相同FVTSpaceDescription的一个空间,这个空间包括多个FAllocatedVirtualTexture,然后需要提出一个Unreal的新概念——Layer。一个FVirtualTextureSpace下有多层Layer,Layer之间是同UV的,这样可以减少同UV的VT的地址转换。FVirtualTextureSpace还包括VirtualTexture和PageTable相关内容,以及处理PageTable的更新。
5.1 Virtual Texture Allocating
Unreal的Virtual Texture实现不是像传统的VT,一个逻辑VT对应上一个已经存在的大的Texture,而是会将几个Texture合并到一个Virtual Texture上,这里的地址的分配由一个Allocator来实现。这个Allocator的算法有点像Buddy Allocator,只不过是二维的。
首先,先将Virtual Texture的大小Ceil到二次幂的正方形大小,然后在Allocator中申请。假如大小不够了,会调用Grow方法,在小于阈值的情况下增倍总大小;如果够,会尝试逐渐分割大小,直到大小合适,下面是一个比较简单的例子:
5.2 FTexturePageMap
这个类是负责一个Layer的Page Table,包括Page Table的数据结构和Map Page的操作。Virtual Address在代码里面为vAddress,它的编码方式是Morton Order。
这个编码有很多好处,在Update Page Table中,需要有一个很重要的操作,就是当我们得到需要更新的Tile后,我们需要不仅仅更新这个Tile,还需要更新对应的低MIP Level对应的位置的Tile,这样可以减少Texture Poping。这里就需要快速找到与当前更新Tile的所谓子Tiles,它维护了一个叫SortedKeys的数据,这里面的key是编码过后的vAddress和Mip。
用上面的图编码(实际是U32的),如果要找到与vAddress为000001,Mip为1的子Tiles。首先对vAddress操作一下,000001 << (vDimensions * Mip) = 000100。这里vDimensions这里我们默认为2,因为Unreal是支持体纹理的,所以有可能为3。然后再计算一个Mask,~0u << (vDimensions * vLogSize) = 00100,就可以发现使用Mask对左上第二个Quad操作,地址就等于vAddress了,这样就找到了它的所有子Tile。
其实,原理上说,Morton Order可以快速构建四叉树,而我们的MipMap其实结构上就是一个四叉树。这里的相关代码在,ExpandPageTableUpdateMasked和ExpandPageTableUpdatePainters上,这两个方法都是用来做MipMap的Tiles的更新的。
两者的区别是,前者会找出原本低Mip的需要更新的Tile,并剔除掉;后者则是用painters算法来保证正确性,也就是每个Tile可能会被绘制多次,用户可以根据情况选择,一个GPU友好,一个CPU友好。
5.3 PageMap Update
通过Feedback Analysis我们得到需要Update的Tile,然后再通过上面说到的Expand函数补充好MipMap的Page,根据上面的结果就可以开始Update PageMap了。Unreal的做法为,为每个Layer,每个MipMap,Draw需要更新的Page的数量的Instance。然后在VS中改Pixel Position和计算Page的值,具体代码在FVirtualTextureSpace的ApplyUpdates中,Shader在PageTableUpdate.usf里。
六、FVirtualTexturePhysicalSpace
FVirtualTexturePhysicalSpace主要内容是Physical Texture的GPU资源和FTexturePagePool。本身的逻辑比较少,多数逻辑在FTexturePagePool中。
6.1 FTexturePagePool
这个类主要和FTexturePageMap一起负责Mapping的相关逻辑。它的主体是Physical Texture,主要负责Physical Texture Tiles的分配,Physical Address在代码里为pAddress,它的编码方式是X优先的展开到一维。它里面有几个数据结构,其中之一是个二叉堆,这个是它的核心数据结构,是用来实现Physical Textured的Tils的LRU算法的。所有的Tiles的地址会在这个二叉堆中,当申请分配的时候,会得到堆顶的地址,每次操作也会更新这个二叉堆,保证堆顶是最旧被使用到的。还有一个FPageEntry的数组,这是以pAddress为Index,存储所有Physical Texture Tiles,对应还有一个方便用FPageEntry找pAddress的HashTable。还有一个FPageMapping的数组,前面的NumPages的内容索引后面的列表,除了最后一个是存了FreeList,其他存的是每个pAddress的Mapping信息。
6.2 FVirtualTextureProducer
这个负责对Physical Texture Tiles的制造,一个FVirtualTexturePhysicalSpace对应一个FVirtualTextureProducer,主要的逻辑在IVirtualTexture的接口中,包括两个流程,一个是RequestPageData,这个流程主要是负责Tiles的加载过程。一个是ProducePageData,这个流程主要负责更新需要最终拷贝到Physical Texture的Tiles的列表。
Runtime VT的实现比较简单,因为它是实时生成的,只是将需要生成哪些Tiles记录下来就可以了。这里再补充一下Stream VT的RequestPageData,除了上文提到过的加载流程。在RequestPageData流程中,有一个会根据平台来做决策的方案,就是会判断是否支持Persistent Mapped Buffers,这个技术可以Map一次,一直保留Map返回的指针,由于Streaming的原因,这个指针确实有一直到加载完才使用的情况。可惜这个在手机和PC平台是不支持的,甚至相关方法在开源的Unreal中是空实现,只有主机版本才有。开源的版本中的实现是申请了一份临时CPU Buffer,先将加载的放到这个临时Buffer中,在后续流程中再将这份内存拷贝到Physical Texture上,这个就是IVirtualTextureFinalizer的工作。
6.3 IVirtualTextureFinalizer
这个接口负责,将FVirtualTextureProducer整理好的数据最终拷贝到Physical Texture上。Runtime Virtual Texture的流程上文已经提及,就是那三个Pass。Streaming Virtual Texture的流程用到上面Producer提供的那份临时Buffer。这里由于Physical Texture被设置成了TexCreate_ShaderResource,也就是CPU是不可写的,需要有一个中间Staging Texture,先把Buffer拷贝到这个中间Staging Texture,再从这个Staging Texture拷贝到Physical Texture上。
6.4 Virtual Texture Filtering
上文已经提到了,Unreal是支持Bi-linear Filtering、Anisotropic Filtering、Tri-linear Filtering的,如何计算坐标这里就不说了,可以看上篇文章,这里说一下Unreal是如何实现这些Filtering的。Bi-linear Filtering就是用Border来解决的,Anisotropic Filtering的实现是Unreal软计算了Anisotropic的偏移,具体算法在MipLevelAniso2D里,然后通过SampleGrad方法传上dUVdx、dUVdy,让硬件完成Anisotropic Filtering。
Tri-linear Filtering的实现比较Trick,它是用一个噪声去让Mip Level在一个范围内变化,参数是位置和帧数,这样就会让一个像素的采样值在一定时间范围内是变化的,配合上TAA来实现了Tri-linear Filtering。
上文所说的一切,还需要配合Unreal的易用而稳定的多线程框架,内存管理机制,Streaming系统等等。我只是简单介绍了一些点,管中窥豹,Unreal对Virtual Texture的实现,需要引擎大量的基底,而在上面又是每行代码的精益求精。读Unreal的代码往往如沐春风,每读一段就感慨他们对技术的执着,以及与他们的差距。
本文的目的是一个导读性质,如果感兴趣,建议大家自己去看看源码。进行下一步的使用、优化和定制修改。
文末,再次感谢李兵的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)
作者主页:https://www.zhihu.com/people/li-bing-77-8,作者也是U Sparkle活动参与者,UWA欢迎更多开发朋友加入U Sparkle开发者计划,这个舞台有你更精彩!
网友评论