diff --git a/.obsidian/plugins/various-complements/data.json b/.obsidian/plugins/various-complements/data.json index 4e9b34c..5c21be9 100644 --- a/.obsidian/plugins/various-complements/data.json +++ b/.obsidian/plugins/various-complements/data.json @@ -182,6 +182,22 @@ "lastUpdated": 1708493734216 } } + }, + "Rendering": { + "Rendering": { + "currentFile": { + "count": 1, + "lastUpdated": 1708508330502 + } + } + }, + "FUploadingVirtualTexture,用于从磁盘中流送上传": { + "FUploadingVirtualTexture,用于从磁盘中流送上传": { + "currentFile": { + "count": 1, + "lastUpdated": 1708510269638 + } + } } } } \ No newline at end of file diff --git a/03-UnrealEngine/Rendering/RenderingPipeline/VirtualTexture学习笔记.md b/03-UnrealEngine/Rendering/RenderingPipeline/VirtualTexture学习笔记.md index a3689d5..1f4db76 100644 --- a/03-UnrealEngine/Rendering/RenderingPipeline/VirtualTexture学习笔记.md +++ b/03-UnrealEngine/Rendering/RenderingPipeline/VirtualTexture学习笔记.md @@ -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流程。 - URuntimeVirtualTexture(UObject) - FRuntimeVirtualTextureRenderResource - UVirtualTexture(UObject) @@ -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 PageTable0, Texture2D 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. */