vault backup: 2024-02-21 19:07:59

This commit is contained in:
BlueRose 2024-02-21 19:07:59 +08:00
parent eff20979fa
commit 56d69ef380
2 changed files with 167 additions and 2 deletions

View File

@ -182,6 +182,22 @@
"lastUpdated": 1708493734216
}
}
},
"Rendering": {
"Rendering": {
"currentFile": {
"count": 1,
"lastUpdated": 1708508330502
}
}
},
"FUploadingVirtualTexture用于从磁盘中流送上传": {
"FUploadingVirtualTexture用于从磁盘中流送上传": {
"currentFile": {
"count": 1,
"lastUpdated": 1708510269638
}
}
}
}
}

View File

@ -7,7 +7,7 @@ rating: ⭐
---
# 前言
- UE4 Runtime Virtual Texture 实现机制及源码解析:https://zhuanlan.zhihu.com/p/143709152
- UE Virtual Texture图文浅析:https://zhuanlan.zhihu.com/p/642580472
- **UE Virtual Texture图文浅析**:https://zhuanlan.zhihu.com/p/642580472
## 相关概念
- Virtual Texture虚拟纹理以下简称 VT
@ -18,7 +18,72 @@ rating: ⭐
- PageTable Texture包含虚拟纹理页表数据的纹理资源通过此纹理资源可查询 Physical Texture Page 信息。有些 VT 系统也叫 Indirection Texture由于本文分析 UE4 VT 的内容,这里采用 UE4 术语。
- PageTable Buffer包含虚拟纹理页表数据内容的 GPU Buffer 资源。
相关类:
### 地址映射
地址映射在Virtual Texture是一个很重要的环节就是如何将一个Virtual Texture的texel映射到Physical Texture的texel上这里还需要适配当高分辨率的page没有加载的情况需要得到已经加载的对应低分辨率的page地址。
#### 四叉树映射
![](https://pic1.zhimg.com/80/v2-754fb67195882775cb95dcb1d2366ad8_720w.webp)
这里每个四叉树的节点的内容存的就是bias和scale,这样就可以将虚拟纹理的地址转换成物理纹理的地址了假如没有找到也可以用父节点的地址来得到低分辨率的。但是这里要找到对应的节点需要搜索这个四叉树搜索的速度取决于树的高度也就是mipmap的层级在差的低mip的地址上效率会比较差。
###  Feedback Rendering
在Virtual Texture中一个很重要的事情是要有一个可以决定加载哪些page的策略这个策略就是要有一个叫Feedback Rendering的过程。这个可以设计为一个单独的pass或者跟Pre-Z或者GBuffer同时。渲染生成的这张texture里面存的就是虚纹理的page坐标mip level和可能的虚纹理id用来应对多虚纹理的情况
![](https://pic1.zhimg.com/80/v2-d74b3fb551b162d4fadb0fb82c299560_720w.webp)
可以看到上图由于page的变化在屏幕空间是比较低的所以Feedback的RT是不需要全分辨率的低分辨率渲染就可以了。对于半透明物体或者alpha test的物体在Feedback Rendering的过程中只能当作是不透明物体来渲染那样就会在屏幕像素上随机产生当前像素的可能结果。与之相类似的如果一个屏幕像素用到了两个page也会是随机出现一种在最后的结果RT上。这样虽然可以让所有需要的page都加载但是可能会遇到另外一个问题那就是可能会发生这一帧加载的page下一帧的时候被卸载掉了然后再下一帧又要加载这样会导致物理纹理一直在置换即便屏幕像素并未改变物理纹理的page也无法稳定下来。为了解决这个问题需要设计一个调色板对于半透明物体间隔出现全透明或者不透明对于多page的情况则需要设计为间隔出现不同page的结果这样就能同时加载所有page并且保持稳定。但是如果出现了多层半透明物体的叠加或者多个page的情况如何设计一个合理的调色板变成了一个问题。这里可以考虑为每个像素匹配一个linked list这个需要额外的硬件支持叫structured append and consume buffers。
接着就是对这个Feedback的结果进行分析了分析需要将Feedback的结果读回CPU这里可以使用compute shader解析这个结果然后输出到一个更简单的buffer上去
![](https://pic3.zhimg.com/80/v2-6a057ea34ed1ee004892220798ea01ae_720w.webp)
这样可以使回读操作更快处理page更新也能更快。对于如何更新page也需要策略我们需要尽量不阻塞执行异步的加载page但是对于完全不存在任何一个mip的page我们还是需要同步加载防止显示出错。在异步的过程中我们需要对需要加载page设置优先级比如需要加载的mip level和已经存在的mip level相差越大的优先级越高被越多像素要求加载的page的优先级越高这里需要设计一个完整的加载策略。
### Texture Poping
由于page是异步加载的这是有延时的当加载的mip比当前显示的差很远的时候我们渲染会使用新加载的更清晰的mip,这样我们会看到非常明显的跳变。假如我们用了软实现的Tri-linear Filtering那么当加载的mip level跟当前显示的mip level相差很大的时候需要做一个delay等待中间的mip page的加载然后再去更新。对于没有Tri-linear Filtering的实现就得逐渐更新page使得过度平滑。一个可能的方法是upsample低分辨率的mip直到高分辨率的mip加载。但是这样仍然会出现跳变由于采样的位置其实发生了突变。
![](https://pic1.zhimg.com/80/v2-7019ba61ac6c2607e994096ede9a6aa0_720w.webp)
上图可以看到当分辨率增加2倍之后结果会发生很大的不同。解决的方案是先把upsample的低分辨率page加载到一个物理纹理的page当高分辨率的加载好了插值过度那个物理纹理的page这样采样的位置没有发生改变只是每个像素的颜色在渐变就不会有跳变出现了。
# UE5VirtualTexture相关实现
为读向往大佬文章的学习笔记。
## 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 基本上也是按照这个原理和流程来实现的,本文就按照这个顺序来详细讲解。在讲解之前,为了便于后续理解,先来了解下 UE5 RVT 的实现机制。
## UE5 RVT 实现机制概述
IVirtualTexture 是 UE5 VT 最重要的接口,它是如何产生 VT 数据的接口,主要有两个抽象函数
- RequestPageData请求页面数据
- ProducePageData产生页面数据
在UE5中其子类有
- FLightmapPreviewVirtualTexture
- FNullVirtualTextureProducer
- FRuntimeVirtualTextureProducer
- FUploadingVirtualTexture
- FVirtualTextureAddressRedirect
- FVirtualTextureLevelRedirector
对于 RVT 来说,实现此接口的是 FRuntimeVirtualTextureProducer也就是作为运行时产生 Page 纹理数据的类,对于 SVT 来说,实现此接口的是 FUploadingVirtualTexture用于从磁盘中流送上传 Page 纹理数据。
FVirtualTextureSystem 是全局单件类,包含了 UE5 VT 系统中大部分核心逻辑和流程,驱动这个系统工作的是 Update 函数,分别在 PC/Console 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 Buffer/Texture 包含在 FVirtualTextureSpace 类中。
FVirtualTextureSystem的会提交请求最终会调用**FRuntimeVirtualTextureProducer::ProducePageData()** ,最后会在**FRuntimeVirtualTextureFinalizer::Finalize()** 中 调用 **RuntimeVirtualTexture::RenderPages()** 函数渲染到 VT Physical Texture 上。
## UE5中相关类
- **FVirtualTextureSystem**单例类用于全局控制VT流程。
- URuntimeVirtualTextureUObject
- FRuntimeVirtualTextureRenderResource
- UVirtualTextureUObject
@ -377,6 +442,90 @@ void SampleRVT_{ParameterName}(in float3 WorldPosition, out bool bInsideVolume,
}
```
RVT的UV计算逻辑在VirtualTextureWorldToUV()中:
```glsl
float2 VirtualTextureWorldToUV(in float3 WorldPos, in float3 Origin, in float3 U, in float3 V)
{
float3 P = WorldPos - Origin;
return saturate(float2(dot(P, U), dot(P, V)));
}
```
从代码可以看出,根据当前像素的世界空间位置以及 RVT Volume 原点Volume 左下角、Volume 边界大小的 UV 范围(经过世界旋转变换的 XY 轴乘以 Volume 缩放-即 Volume 大小-的倒数,这些计算在 **URuntimeVirtualTexture::Initialize()** 中完成),求出当前像素在 RVT 中的 UV 坐标。
TextureComputeVirtualMipLevel() 函数计算 RVT 的 mipLevel为了实现较好的混合效果这里根据当前帧 Id 生成交错的随机 noise 扰动 level。
```cpp
int TextureComputeVirtualMipLevel(
in out VTPageTableResult OutResult,
float2 dUVdx, float2 dUVdy, float MipBias,
float2 SvPositionXY,
VTPageTableUniform PageTableUniform)
{
OutResult.dUVdx = dUVdx * PageTableUniform.SizeInPages;
OutResult.dUVdy = dUVdy * PageTableUniform.SizeInPages;
// Always compute mip level using MipLevelAniso2D, even if VIRTUAL_TEXTURE_ANISOTROPIC_FILTERING is disabled
// This way the VT mip level selection will come much closer to HW mip selection, even if we're not sampling the texture using anisotropic filtering const float ComputedLevel = MipLevelAniso2D(OutResult.dUVdx, OutResult.dUVdy, PageTableUniform.MaxAnisoLog2);
const float GlobalMipBias = GetGlobalVirtualTextureMipBias();
#if VIRTUAL_TEXTURE_MANUAL_TRILINEAR_FILTERING
const float Noise = 0.f;
#else
const float Noise = GetStochasticMipNoise(SvPositionXY);
#endif
const float MipLevel = ComputedLevel + MipBias + GlobalMipBias + Noise;
const float MipLevelFloor = floor(MipLevel);
OutResult.MipLevelFrac = MipLevel - MipLevelFloor;
return (int)MipLevelFloor + int(PageTableUniform.vPageTableMipBias);
}
```
TextureLoadVirtualPageTableInternal 函数代码如下:
```cpp
void TextureLoadVirtualPageTableInternal(
in out VTPageTableResult OutResult,
Texture2D<uint4> PageTable0, Texture2D<uint4> PageTable1,
VTPageTableUniform PageTableUniform,
float2 UV, int vLevel)
{
OutResult.UV = UV * PageTableUniform.SizeInPages;
const uint vLevelClamped = clamp(vLevel, 0, int(PageTableUniform.MaxLevel));
uint vPageX = (uint(OutResult.UV.x) + PageTableUniform.XOffsetInPages) >> vLevelClamped;
uint vPageY = (uint(OutResult.UV.y) + PageTableUniform.YOffsetInPages) >> vLevelClamped;
OutResult.PageTableValue[0] = PageTable0.Load(int3(vPageX, vPageY, vLevelClamped));
OutResult.PageTableValue[1] = PageTable1.Load(int3(vPageX, vPageY, vLevelClamped));
#if VIRTUAL_TEXTURE_MANUAL_TRILINEAR_FILTERING
// Second page table for trilinear.
const uint vLevelClamped2 = clamp(vLevel + 1, 0, int(PageTableUniform.MaxLevel));
const uint vPageX2 = (uint(OutResult.UV.x) + PageTableUniform.XOffsetInPages) >> vLevelClamped2;
const uint vPageY2 = (uint(OutResult.UV.y) + PageTableUniform.YOffsetInPages) >> vLevelClamped2;
OutResult.PageTableValue[2] = PageTable0.Load(int3(vPageX2, vPageY2, vLevelClamped2)); OutResult.PageTableValue[3] = PageTable1.Load(int3(vPageX2, vPageY2, vLevelClamped2));
// Alternate requests to both mip levels
if ((View.FrameNumber & 1u) == 0u)
{ vLevel += 1; vPageX = vPageX2; vPageY = vPageY2; }#endif
// PageTableID packed in upper 4 bits of 'PackedPageTableUniform', which is the bit position we want it in for PackedRequest as well, just need to mask off extra bits
OutResult.PackedRequest = PageTableUniform.ShiftedPageTableID;
OutResult.PackedRequest |= vPageX;
OutResult.PackedRequest |= vPageY << 12;
// Feedback always encodes vLevel+1, and subtracts 1 on the CPU side.
// This allows the CPU code to know when we requested a negative vLevel which indicates that we don't have sufficient virtual texture resolution. const uint vLevelPlusOneClamped = clamp(vLevel + 1, 0, int(PageTableUniform.MaxLevel + 1));
OutResult.PackedRequest |= vLevelPlusOneClamped << 24;
}```
这个函数主要 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 中。
VT还存在一个反馈机制具体可以参考[[#Pass1的补充VirtualTextureFeedback]]
```c++
/** GPU fence pool. Contains a fence array that is kept in sync with the FeedbackItems ring buffer. Fences are used to know when a transfer is ready to Map() without stalling. */