产品与解决方案 Unreal Virtual Texture 源码导读

侑虎科技 · 2020年07月20日 · 1603 次阅读

上一篇《浅谈 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-8U,作者也是 Sparkle 活动参与者,UWA 欢迎更多开发朋友加入 U Sparkle 开发者计划,这个舞台有你更精彩!

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册