bilibili在线解析接口_游戏引擎随笔 0x14:UE4 Runtime Virtual Texture 实现机制及源码解析...

46a0d7427352bf2ea2d3927610a327ab.png

前言

Unreal Engine 从 4.23 开始增加了 Virtual Texture 系统,并提供了 2 种 Virtual Texture 机制:Streaming Virtual Texture 和 Runtime Virtual Texture。经过 3 个版本的迭代和完善,在最新的 4.25 版本中提供了更加完善的功能,同时对移动平台有了更好的支持。

本文主要讲解 UE 4.25 中 Runtime Virtual Texture 系统实现机制以及源码解析,本文不包括 Virtual Texture 原理内容,阅读本文请先了解 Virtual texture 的相关背景知识,另外本文也不包括 UE4 中 Virtual Texture 的使用方面的介绍,所以最好也请先了解 UE4 的 Runtime Virtual Texture 的基础概念以及使用方面的知识。这里有一篇比较好的 Virtual Texture 的原理介绍文章,供大家参考:

李兵:浅谈Virtual Texture​zhuanlan.zhihu.com
9c67a1440b2fa4d8043673057d367ace.png

以及 Epic 官方的 Virtual Texture 技术讲解视频:

[中文直播]第18期 | Virtual Texture(虚拟纹理)的理解和应用 | Epic 李文磊_哔哩哔哩 (゜-゜)つロ 干杯~-bilibili​www.bilibili.com
8898a40931db96ab861375ad83a0e31c.png
由于篇幅限制,本文不可能把 UE4 RVT 所有代码都列出来讲解,对于关键功能会列出核心代码并加以阐述说明,其它非关键部分只列出相关类名和函数,并辅以文字描述。另外虽然本文讲解的是 Runtime Virtual Texture,但 UE4 的实现中 Virtual Texture 的基础以及抽象部分是共用的,因此部分内容对 Streaming Virtual Texture 也适用。

术语

为了便于理解,讲解之前先统一定义文中术语:

  • Virtual Texture:虚拟纹理,以下简称 VT
  • Runtime Virtual Texture:UE4 运行时虚拟纹理系统,以下简称 RVT
  • VT feedback:存储当前屏幕像素对应的 VT Page 信息,用于加载 VT 数据。
  • VT Physical Texture:虚拟纹理对应的物理纹理资源
  • PageTable:虚拟纹理页表,用来寻址 VT Physical Texture Page 数据。
  • PageTable Texture:包含虚拟纹理页表数据的纹理资源,通过此纹理资源可查询 Physical Texture Page 信息。有些 VT 系统也叫 Indirection Texture,由于本文分析 UE4 VT 的内容,这里采用 UE4 术语。
  • PageTable Buffer:包含虚拟纹理页表数据内容的 GPU Buffer 资源。

VT 系统概述

从原理上来说,VT 系统主要由 2 大阶段构成,VT 数据准备和 VT 运行时采样阶段。

  1. VT 数据准备阶段:
    1. 生成 VT feedback 数据
    2. 生成 VT 纹理数据,并更新到指定 VT Physical Texture 对应的 Page
    3. 根据 feedback 数据生成并更新 PageTable 数据
  2. VT 运行时采样阶段:
    1. 根据屏幕空间坐标以及相关信息生成 VT Physical Texture UV
    2. 对 VT Physical Texture 执行采样

UE4 的 RVT 基本上也是按照这个原理和流程来实现的,本文就按照这个顺序来详细讲解。在讲解之前,为了便于后续理解,先来了解下 UE4 RVT 的实现机制。

UE4 RVT 实现机制概述

IVirtualTexture 是 UE4 VT 最重要的接口,它是如何产生 VT 数据的接口,主要有两个抽象函数

  • RequestPageData,请求页面数据
  • ProducePageData,产生页面数据

对于 RVT 来说,实现此接口的是 FRuntimeVirtualTextureProducer,也就是作为运行时产生 Page 纹理数据的类,对于 SVT 来说,实现此接口的是 FUploadingVirtualTexture,用于从磁盘中流送上传 Page 纹理数据。

FVirtualTextureSystem 是全局单件类,包含了 UE4 VT 系统中大部分核心逻辑和流程,驱动这个系统工作的是 Update 函数,分别在 PCConsole Pipeline 的 FDeferredShadingSceneRenderer::Render 和 Mobile Pipeline 的 FMobileSceneRenderer::Render 中调用,具体细节会在下文中详细讲解。

在 VT 中只有 Diffuse 是远远不够的,在实际的着色过程中经常需要其它的纹理数据来进行光照计算,比如 Normal、Roughness、Specular 等等,UE4 的 RVT 使用了 Layer 的概念,每个 Layer 代表不同的 Physical Texture,在 UE4 中可以支持底色(Diffuse)、法线(Normal)、Roughness(粗糙度)、高光度(Specular)、遮罩(Mask)等不同内容的 VT,这些数据以 Pack 的方式保存在多张 Physical Texture 的不同通道中,在使用时通过 Unpack 以及组合的形式解算出来进行光照计算。这些 Physical Texture 的寻址信息保存在同一个 VT 上的 PageTable Texture 的不同颜色通道中,下文会详细描述。

UE4 RVT 中所使用的 GPU 资源有以下 3 种:

  • PageTable Buffer 用于在 CPU 端只写的 PageTable 数据。
  • PageTable Texture 用于在采样 VT 时获取指定 VT Physical Texture Page 数据,此资源数据不是在 CPU 端填充,而是由 PageTable Buffer 通过 RHICommandList 在 GPU 上填充。
  • VT Physical Texture 实际存储纹理数据的资源,通过 VT feedback 从磁盘或运行时动态生成纹理数据,并在 VT 数据准备阶段中更新到 VT Physical Texture 中。

其中 VT Physical Texture 资源包含在 FVirtualTexturePhysicalSpace 类中,PageTable BufferTexture 包含在 FVirtualTextureSpace 类中。


一、VT 数据准备阶段

1、生成 VT feedback

与一般在 GBuffer pass 中生成 VT feedback Buffer 不同的是,UE4 中并没有单独的 VT feedback Pass,而是在材质 Shader 的最后调用 FinalizeVirtualTextureFeedback 将当前帧的 Request Page 信息 写入 feedback UAV Buffer,然后在 CPU 侧每帧的 FVirtualTextureSystem::Update 中读取这个 Buffer,根据 Buffer 中的 Request Page 信息读取 Page Texture 数据。

FinalizeVirtualTextureFeedback 函数被不同的 Render pipeline 调用,比如 PCConsole 的 BasePassPixelShader 和 Mobile 的 MobileBasePassPixelShader。这个函数很简单,主要是把 PS 生成的 VT Request 写入到 UAV Buffer 中,部分代码如下:

void 
出于性能考虑,并不是写入与屏幕分辨率相同大小的 Buffer,而是根据项目中的反馈分辨率因子设置来写入,对应 Shader 中的 VIRTUAL_TEXTURE_FEEDBACK_FACTOR,这个值越大性能越好,但粒度越粗,很可能会漏掉 VT 数据而导致渲染不正确。

2、生成 VT 纹理数据

GPU 侧生成好 feedback Buffer 之后,在 CPU 侧的 VirtualTextureSystem::Update 函数通过 FSceneRenderTargets 单件类的 VirtualTextureFeedback 成员,回读 VT feedback 数据,根据回读到的 Request 数据,然后将搜集好的数据通过 Task Graph 系统进行并行分析处理,代码如下:

for 

在 FFeedbackAnalysisTask 中最终调用 FVirtualTextureSystem::FeedbackAnalysisTask 处理 Request 数据,函数代码如下:

void 

可以看出是将 GPU 产生的 Request 数据加入到 FUniquePageList 中,并统计出现的次数。FUniquePageList 内部是一个 Hash Table,通过 hash Page Request 得到 Page 的索引并进行累加次数。

等待所有分析任务完成之后,再去除重复的 Page,生成唯一的 Page list:

for 

接下来是生成唯一的请求列表 FUniqueRequestList,流程和 FUniquePageList 类似,根据上一步得到的 FUniquePageList 再次通过 TaskGraph 并行处理,在 FVirtualTextureSystem::SubmitRequests 中提交请求列表,最终调用到 FRuntimeVirtualTextureProducer::ProducePageData,产生 FRuntimeVirtualTextureFinalizer 对象,在 FRuntimeVirtualTextureFinalizer::Finalize 中 调用 RuntimeVirtualTexture::RenderPages 函数渲染到 VT Physical Texture 上。

FRuntimeVirtualTextureFinalizer::Finalize 中主要生成以 VT Physical Texture 为批次的 FRenderPageBatchDesc 数据,遍历需要生成纹理数据的 VT Pages,当遇到不同的 Physical Texture 则打断批次,并执行 RenderPages 。在 RenderPages 中遍历每个 Page 调用 RenderPage 函数执行真正的生成 VT 纹理数据功能,部分代码如下:

bool 

RenderPage 使用 RenderDependencyGraph 协助完成 VT 纹理数据的生成。这个函数主要有三个 Pass 构成:

  1. Draw Pass,通过 RTT 方式生成 VT 纹理数据,对应 DrawMeshes 函数。
  2. Compression Pass,将生成的纹理数据使用 CS 生成 GPU 支持的压缩格式,对应 AddCompressPass 函数。
  3. Copy Pass,将纹理数据进一步编码到更少的 RT 中,减少资源占用,对应 AddCopyPass 函数。

为了方便说明,这里只列出关键代码段,并在前面标注了序号

void 

下面逐条进行解析:

序号 1 FRenderGraphSetup 根据 RVT Asset 配置的内容生成 RDG 资源,比如选择 底色、法线、粗糙度、高光度 会生成 3 张 B8G8R8A8 格式的 RenderTarget,用于生成 Diffuse、Normal、Specular/Roughness 数据。FRenderGraphSetup 包含了 3 个 Pass 所需要的所有 RGD 资源,并且为了节省 GPU 内存,使用了重叠资源。

序号 2 DrawMeshes 搜集所有在当前 RVT Volume 范围内的 Mesh 进行绘制。对应绘制的 Shader 是 VirtualTextureMaterial.usf,在 PS 中通过 MRT 输出到对应的 RT 中,部分代码如下:

void 

由代码可以看出,这里直接调用 Mesh 材质 Shader 的各个通道输出作为结果,存储到对应的 RT 中。

序号 3 AddCompressPass 通过 CS 将在第 2 步生成的 B8G8R8A8 格式的纹理数据生成 GPU 压缩格式,这一步都在 VirtualTextureCompress.usf Shader 中处理,不同的 VT 配置使用不同的 CS 函数执行块压缩,包括 BC3, BC5, BC1,目前不支持移动平台的 ETC 压缩。

序号 4 AddCopyPass 将第 3 步压缩后的 RT 进行合并,这一步也是在 VirtualTextureCompress.usf Shader 中完成,以底色、法线、粗糙度、高光度为例,读取 3张 texture,packed 并写入到 2 个 RT 中,代码如下:

/** Copy path used when we disable texture compression, because we need to keep the same final channel layout. */

序号 5 这一步很简单,只是调用图形 API Copy 命令将最终渲染生成的 VT 纹理数据 Copy 到 VT Physical Texture 的对应的 Page (区域)上。

至此 RVT 的纹理数据生成完毕,接下来是更新 PageTable 数据。

3、PageTable Texture 的更新

在生成 VT 纹理数据的最后( FVirtualTextureSystem::SubmitRequests 函数的最后),调用 FVirtualTextureSpace::ApplyUpdates 更新 PageTable Buffer 并渲染到 PageTable Texture 中。

在 FVirtualTextureSpace::ApplyUpdates 中遍历每个 PageTable Layer,通过 Layer 对应的 FTexturePageMap 生成 FPageTableUpdate 数据结构,这个结构包含了需要更新的 PageTable 原数据,为了减少 GPU 资源开销,UE4 创建 64bit R16G16B16A16_UINT 格式 GPU Buffer 存储这个数据,这就是 PageTable Buffer。然后通过 RHI 的 Lockunlock 操作将数据更新到这个 Buffer 中,代码如下:

uint8

更新好 PageTable Buffer 之后,创建 RVT 内容类型对应的格式的 PageTable RenderTarget,遍历 PageTable RenderTarget 的每个 mip level,直接调用 FRHICommandList 执行渲染操作,将 PageTable Buffer 中数据写入到这个 RT 中。渲染部分代码如下:

uint32 
值得一提的是,UE4 并没有使用 Compute pipeline 来更新 PageTable Texture,而是通过 Graphics Pipeline 在 VS 中生成,在 PS 中存储到 PageTable RenderTarget,并且使用 Instancing Rendering 的方式,最多一次处理 16 个 PageTable Quad,这样处理与 CS 基本相同,至于为何不直接用 CS,我猜测也许因为前面生成 VT 纹理数据时已经使用了 CS,为了提高 GPU 的并行性而使用 Graphics Pipeline,不过这种优化在 PCConsole 上是可行的,对移动平台却并不能带来提升,如代码所示,使用 Graphics Pipeline 就需要对每个 mip level 要执行一次 RenderPass,对移动平台上的现代 API 来说,这样并不是 GPU Friendly 的,而且在某些 GPU 比如 Mali 上,VS 和 CS 共用同一个硬件 Shader Core 单元,并不能带来期望的 CS 和 VS 并行,因此这部分代码可以进一步改进,针对移动平台特性编写单独的代码路径来优化。

UE4 将 PageTable 数据渲染到 texture 主要目的是为了在使用 VT 时能够快速寻址,渲染的 Shader 代码在 PageTableUpdate.usf 中,其中 PageTableUpdateVS 函数将 Page 的 Level、Page XY 信息 Pack 到 PageTable Texture 中,部分代码如下:

#if USE_16BIT

RVT 的使用

VT 的数据准备好之后,就是如何使用了。RVT 的使用主要是通过在 Material 的 Shader 中,加入 RVT 相关材质节点来采样 RVT 来完成的。下面是在 Material Editor 中使用 Runtime Virtual Texutre Sample 节点生成的 HLSL 使用 RVT 部分的代码:

VTPageTableResult 

这里主要调用了 2 个 Shader 函数:TextureLoadVirtualPageTable 和 TextureVirtualSample。TextureLoadVirtualPageTable 函数用于生成 VTPageTableResult 结构,这个结构包含了间接寻址的 Page 数据;TextureVirtualSample 函数中使用这个结构执行真正的纹理采样工作。

TextureLoadVirtualPageTable

分析第一个函数 TextureLoadVirtualPageTable,代码如下:

VTPageTableResult 

其中 UV 参数是 RVT 的 UV 坐标,那么如何获取 VT 的 UV 呢?答案在 VirtualTextureWorldToUV 函数中:

float2 

从代码可以看出,根据当前像素的世界空间位置以及 RVT Volume 原点(Volume 左下角)、Volume 边界大小的 UV 范围(经过世界旋转变换的 XY 轴乘以 Volume 缩放-即 Volume 大小-的倒数,这些计算在 URuntimeVirtualTexture::Initialize 中完成),求出当前像素在 RVT 中的 UV 坐标。

TextureComputeVirtualMipLevel 函数计算 RVT 的 mipLevel,为了实现较好的混合效果,这里根据当前帧 Id 生成交错的随机 noise 扰动 level,代码如下:

uint 

TextureLoadVirtualPageTableInternal 函数代码如下:

void 

这个函数主要 2个作用,一是生成用于寻址 VT Physical Texture 的 PageTableValue,另一个是生成 feedback Request 数据,具体有以下几个步骤:

  1. 根据 UV 寻址模式修正虚拟纹理坐标
  2. 根据当前 VT 的 Page 数量和上一步修正过的虚拟纹理坐标计算出 VT 坐标对应的 Page 坐标。
  3. 通过 Page 坐标加上 Page 的 XY 偏移,再根据 mipLevel,计算出 PageTable Texture 的 UV 坐标,然后使用这个 UV 坐标和 mipLevel 采样 PageTable Texture 得到在 Physical Texture 上的信息,保存在 PageTableValue 中,在接下来的流程中使用。
  4. 将第 3 步计算好的 PageTable Texture 的 Page 坐标和 mipLevel 保存在 VTPageTableResult 中,最后通过 StoreVirtualTextureFeedback 函数写入到 VT feedback Buffer 中。

TextureVirtualSample

采样所需的 VTPageTableResult 数据准备完毕,在 TextureVirtualSample 函数中就是执行真正的 Physical Texture 采样逻辑,代码如下:

MaterialFloat4 

这个函数很简单,只有 2 个函数调用,第一行 VTComputePhysicalUVs 用于生成 Physical Texture UV 坐标,第二行用于执行渐变采样,所以这里重点是如何生成 Physical Texture UV 坐标,VTComputePhysicalUVs 函数代码如下:

float2 

这个函数通过在 TextureLoadVirtualPageTableInternal 中采样 PageTable Texture 得到在 Physical Texture 上的信息 PackedPageTableValue,计算出采样 Physical Texture 的 mipLevel 和 UV 坐标,步骤如下:

  1. PackedPageTableValue 低 4bit 得到 mipLevel,最多 16 级(0~15)
  2. 由 mipLevel 计算 UV Scale
  3. 根据 PackedPageTableValue 的中高 8 位(32bit PageTable Texture)或 6 位(16bit PageTable Texture)计算出在 Physical Texture 的 Page 坐标。
  4. 根据第 2 步的 VU Scale 计算出 mipLevel 对应的 UV 坐标,取小数部分 UV 坐标既是在 Page 内的 UV 坐标。
  5. 根据第 3 步的 page 坐标乘以每 Page 像素大小在整个 Physical Texture 像素大小的比值的倒数,得到 Page 在 Physical Texture 的起始 UV 坐标,加上把第 4 步的 Page 内 UV 缩放到整个 Physical Texture UV 坐标,再加上 Page border 与 Physical Texture 像素比值的倒数,就得出最终的 Physical Texture UV 采样的坐标。
  6. 将第 2 步的 UV Scale 缩放到整个 Physical Texture,来计算最终在 Physical Texture 的 xy 方向的导数,作为最终采样时使用。

UE4 RVT 资源格式及布局

RVT 的布局配置中选择不同的虚拟纹理内容,将决定 VT Physical Texture 和 PageTable Texture 的像素格式和布局。

98a4c2f532d0308d337208c52360a7e0.png
RVT 的布局配置中虚拟纹理内容选项

在代码中根据内容枚举类型返回 Texture 层数,如下所示:

int32 

如代码所示,虚拟纹理内容类型和层数之间对应关系如下:

  • 基础颜色或者场景高度,返回 1层
  • 底色、法线、粗糙度、高光度,返回 2 层
  • YCoCg 底色、法线、粗糙度、高光度或 遮罩,返回 3 层

在 UE4 中层数决定了 VT Physical Texture 的数量。

VT Physical Texture 像素格式及存储布局

VT Physical Texture 像素格式与 RVT 的配置有关,当需要 2 层纹理时(即底色、法线、粗糙度、高光度类型),FVirtualTexturePhysicalSpace 会分配 2 个 VT Physical Texture,纹理数据布局如下:

  • #0 Texture (DXT1格式)的 rgb 通道存储底色
  • #0 Texture 的 a 通道存储 法线 x 分量
  • #1 Texture (DXT5格式)的 a 通道存储 法线 y 分量
  • #1 Texture 的 b 通道存储 法线 z 分量,带符号
  • #1 Texture 的 r 通道存储 高光度
  • #1 Texture 的 g 通道存储 粗糙度
在 Shader 中调用 VirtualTextureUnpackNormalBC3BC3 函数 Unpack Normal 数据。

当当需要 3 层纹理时(即 YCoCg底色、法线、粗糙度、高光度(遮罩)),FVirtualTexturePhysicalSpace 会分配 3 个 VT Physical Texture,纹理数据布局如下:

  • #0 Texture (DXT1格式)的 rgba 通道存储 YCoCg 空间底色。
  • #1 Texture (BC5格式)的 rg 通道存储 法线 xy 分量
  • #2 Texture (DXT1格式遮罩 DXT5格式)的 b 通道存储 法线 z 分量,带符号
  • #2 Texture 的 a 通道存储 遮罩值
  • #2 Texture 的 r 通道存储 高光度
  • #2 Texture 的 g 通道存储 粗糙度
在 Shader 中调用 VirtualTextureUnpackBaseColorYCoCg 函数 Unpack Diffuse 数据。在 Shader 中调用 VirtualTextureUnpackNormalBC5BC1 函数 Unpack Normal 数据。

在 PC 或主机平台上 VT Physical Texture 根据 RVT 内容配置不同采用不同的 GPU 压缩格式,比如如果内容只是底色,则使用 DXT1 格式,如果内容是 底色、法线、粗糙度、高光度 则使用 DXT5,如果是 YCoCg 空间则是 BC5,以满足 unpack 法线数据时的精度要求。需要注意的是,目前在移动平台上还不支持 GPU 压缩格式。

PageTable Texture 像素格式及布局

当 VT Physical Texture 中的 Page 不超过 64 * 64 个时,PageTable Texture 使用 16bit 格式,因为只需要记录 0~15(4位)mip level,以及 0~63(6位)个 Page 的 X、Y 坐标,总计 16 bit,否则使用 32bit,其中 4 bit(0~15)mip level,以及 8 bit(0~255)个 Page 的 X、Y 坐标。

由于 GPU 硬件的限制,尤其在移动平台,最大支持 4k 纹理,如果每个 Page 是 128 大小,则一个 Physical Texture 最多有 32 个 Page,所以一般情况下都是 16bit 格式。但是 PageTable Texture 却可以使用 4k 的大小,也就意味着可以索引 4k 个 Physical Texture Page,因此在 UE4 中一个 PageTable Texture 可以索引多张 Physical Texture。

出于性能优化的考虑,UE4 的 RVT 将不同 Layer 的 Physical Texture Page 数据存储在 1 个 PageTable Texture 的各个颜色通道中,当在 RVT 的配置中选择不同内容布局时,PageTable Texture 的像素格式也会随之变化,这样在获取 Physical Texture Page 数据时,只需要对 PageTable Texture 采样一次即可获取每个 Layer 的 Physical Texture Page 信息。在 VirtualTextureSpace 中 GetFormatForNumLayers 函数根据 Texture 层数和 Page 格式返回 PageTable Texture 的像素格式,代码如下所示:

VirtualTextureSpace

后记

在 VT 实现过程中,往往由于工程化程度不够而导致无法实用,UE4 的 VT 使我们看到一个真正意义上工程化且实用的 VT 是如何实现的,分析它一方面对于更深入的了解 UE4 VT 有很好的帮助,另一方面对实现自己的 VT 系统也有很好的工程化参考意义,尤其是一些优化手段。最后需要说明的是,UE4 的 VT 在实现过程中加入了大量的利于工程化的优化机制和手段,因此实际的代码要远比文中列出的庞杂,本文只是对关键部分的代码加以分析和说明,如果要了解更多的实现细节,可以按照文中梳理的脉络来阅读源码,相信会有更多收获。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值