UE4 Runtime Virtual Texture实现机制及源码解析
综合文库 400 0
实名

通过了实名认证的内容创造者

发布于 2020-8-10 09:18:31

您需要 登录 才可以下载或查看,没有账号?注册

x
一、前言
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》以及Epic官方的Virtual Texture技术讲解视频,供大家参考。

由于篇幅限制,不可能把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运行时采样阶段。
  • VT数据准备阶段:
    a. 生成VT feedback数据。
    b. 生成VT纹理数据,并更新到指定VT Physical Texture对应的Page。
    c. 根据Feedback数据生成并更新PageTable数据。
  • VT运行时采样阶段:
    a. 根据屏幕空间坐标以及相关信息生成VT Physical Texture UV。
    b. 对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类中。
1. VT数据准备阶段
1.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 FinalizeVirtualTextureFeedback(in FVirtualTextureFeedbackParams Params, float4 SvPosition, float Opacity, uint FrameNumber, RWBuffer OutputBuffer)        {            uint2 PixelPos = SvPosition.xy;             uint FeedbackPos = 0;            // This code will only run every few pixels...            [branch] if (((PixelPos.x | PixelPos.y) & (VIRTUAL_TEXTURE_FEEDBACK_FACTOR-1)) == 0)            {                // TODO use append buffer ?                PixelPos /= VIRTUAL_TEXTURE_FEEDBACK_FACTOR;                FeedbackPos = PixelPos.y*View.VirtualTextureFeedbackStride + PixelPos.x;                                ...                ...                OutputBuffer[FeedbackPos] = Params.Request;            }           }}
出于性能考虑,并不是写入与屏幕分辨率相同大小的Buffer,而是根据项目中的反馈分辨率因子设置来写入,对应Shader中的VIRTUAL_TEXTURE_FEEDBACK_FACTOR,这个值越大性能越好,但粒度越粗,很可能会漏掉VT数据而导致渲染不正确。

1.2 生成 VT 纹理数据
GPU侧生成好Feedback Buffer之后,在CPU侧的VirtualTextureSystem::Update函数通过FSceneRenderTargets单件类的VirtualTextureFeedback成员,回读VT Feedback数据,根据回读到的Request数据,然后将搜集好的数据通过Task Graph系统进行并行分析处理,代码如下:
for (int32 TaskIndex = 0; TaskIndex < LocalFeedbackTaskCount; ++TaskIndex)    {        FFeedbackAnalysisTask::DoTask(FeedbackAnalysisParameters[TaskIndex]);    }    if (WorkerFeedbackTaskCount > 0)    {        SCOPE_CYCLE_COUNTER(STAT_ProcessRequests_WaitTasks);        FTaskGraphInterface::Get().WaitUntilTasksComplete(Tasks, ENamedThreads::GetRenderThread_Local());    }
在FFeedbackAnalysisTask中最终调用FVirtualTextureSystem::FeedbackAnalysisTask处理Request数据,函数代码如下:
void FVirtualTextureSystem::FeedbackAnalysisTask(const FFeedbackAnalysisParameters& Parameters){    FUniquePageList* RESTRICT RequestedPageList = Parameters.UniquePageList;    const uint32* RESTRICT Buffer = Parameters.FeedbackBuffer;    const uint32 Width = Parameters.FeedbackWidth;    const uint32 Height = Parameters.FeedbackHeight;    const uint32 Pitch = Parameters.FeedbackPitch;    // Combine simple runs of identical requests    uint32 LastPixel = 0xffffffff;    uint32 LastCount = 0;    for (uint32 y = 0; y < Height; y++)    {        const uint32* RESTRICT BufferRow = Buffer + y * Pitch;        for (uint32 x = 0; x < Width; x++)        {            const uint32 Pixel = BufferRow[x];            if (Pixel == LastPixel)            {                LastCount++;                continue;            }            if (LastPixel != 0xffffffff)            {                RequestedPageList->Add(LastPixel, LastCount);            }            LastPixel = Pixel;            LastCount = 1;        }    }    if (LastPixel != 0xffffffff)    {        RequestedPageList->Add(LastPixel, LastCount);    }}
可以看出是将GPU产生的Request数据加入到FUniquePageList中,并统计出现的次数。FUniquePageList内部是一个Hash Table,通过Hash Page Request得到Page的索引并进行累加次数。
等待所有分析任务完成之后,再去除重复的Page,生成唯一的Page List:
for (uint32 TaskIndex = 1u; TaskIndex < NumFeedbackTasks; ++TaskIndex){    MergedUniquePageList->MergePages(FeedbackAnalysisParameters[TaskIndex].UniquePageList);}
接下来是生成唯一的请求列表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 bBreakBatchForTextures = false;    for (int LayerIndex = 0; LayerIndex < RuntimeVirtualTexture::MaxTextureLayers; ++LayerIndex)    {        // This should never happen which is why we don't bother sorting to maximize batch size        bBreakBatchForTextures |= (RenderPageBatchDesc.Targets[LayerIndex].Texture != Entry.Targets[LayerIndex].TextureRHI);    }    if (++BatchSize == RuntimeVirtualTexture::EMaxRenderPageBatch || bBreakBatchForTextures)    {        RenderPageBatchDesc.NumPageDescs = BatchSize;        RuntimeVirtualTexture::RenderPages(RHICmdList, RenderPageBatchDesc);        BatchSize = 0;    }    if (bBreakBatchForTextures)    {        for (int LayerIndex = 0; LayerIndex < RuntimeVirtualTexture::MaxTextureLayers; ++LayerIndex)        {            RenderPageBatchDesc.Targets[LayerIndex].Texture = Tiles[0].Targets[LayerIndex].TextureRHI != nullptr ? Tiles[0].Targets[LayerIndex].TextureRHI->GetTexture2D() : nullptr;            RenderPageBatchDesc.Targets[LayerIndex].UAV = Tiles[0].Targets[LayerIndex].UnorderedAccessViewRHI;        }    }
RenderPage使用RenderDependencyGraph协助完成VT纹理数据的生成。这个函数主要有三个Pass构成:
  • Draw Pass,通过RTT方式生成VT纹理数据,对应DrawMeshes函数。
  • Compression Pass,将生成的纹理数据使用CS生成GPU支持的压缩格式,对应AddCompressPass函数。
  • Copy Pass,将纹理数据进一步编码到更少的RT中,减少资源占用,对应AddCopyPass函数。
为了方便说明,这里只列出关键代码段,并在前面标注了序号:
void RenderPage(...){...    // Build graph    FMemMark Mark(FMemStack::Get());    FRDGBuilder GraphBuilder(RHICmdList);1    FRenderGraphSetup GraphSetup(GraphBuilder, MaterialType, OutputTexture0, TextureSize, bIsThumbnails);    // Draw Pass    if (GraphSetup.bRenderPass)    {        FShader_VirtualTextureMaterialDraw::FParameters* PassParameters = GraphBuilder.AllocParameters();                ...        GraphBuilder.AddPass(            RDG_EVENT_NAME("VirtualTextureDraw"),            PassParameters,            ERDGPassFlags::Raster,            [Scene, View, MaterialType, RuntimeVirtualTextureMask, vLevel, MaxLevel](FRHICommandListImmediate& RHICmdListImmediate)        {2            DrawMeshes(RHICmdListImmediate, Scene, View, MaterialType, RuntimeVirtualTextureMask, vLevel, MaxLevel);        });    }    // Compression Pass    if (GraphSetup.bCompressPass)    {        FShader_VirtualTextureCompress::FParameters* PassParameters = GraphBuilder.AllocParameters();        ...3        AddCompressPass(GraphBuilder, View->GetFeatureLevel(), PassParameters, TextureSize, MaterialType);    }    // Copy Pass    if (GraphSetup.bCopyPass || GraphSetup.bCopyThumbnailPass)    {        FShader_VirtualTextureCopy::FParameters* PassParameters = GraphBuilder.AllocParameters();        ...        if (GraphSetup.bCopyPass)        {4            AddCopyPass(GraphBuilder, View->GetFeatureLevel(), PassParameters, TextureSize, MaterialType);        }        else        {            AddCopyThumbnailPass(GraphBuilder, View->GetFeatureLevel(), PassParameters, TextureSize, MaterialType);        }    }        ....        ....    // Execute the graph    GraphBuilder.Execute();    ...    // Copy to final destination    if (GraphSetup.OutputAlias0 != nullptr && OutputTexture0 != nullptr)    {        FRHICopyTextureInfo Info;        Info.Size = GraphOutputSize0;        Info.DestPosition = FIntVector(DestBox0.Min.X, DestBox0.Min.Y, 0);5        RHICmdList.CopyTexture(GraphOutputTexture0->GetRenderTargetItem().ShaderResourceTexture->GetTexture2D(), OutputTexture0->GetTexture2D(), Info);    }    ...}
下面逐条进行解析:
序号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 FPixelShaderInOut_MainPS(...){        ....    // Output is from standard material output attribute node    half3 BaseColor = GetMaterialBaseColor(PixelMaterialInputs);    half Specular = GetMaterialSpecular(PixelMaterialInputs);    half Roughness = GetMaterialRoughness(PixelMaterialInputs);    half3 Normal = MaterialParameters.WorldNormal;    float WorldHeight = MaterialParameters.AbsoluteWorldPosition.z;    float Opacity = GetMaterialOpacity(PixelMaterialInputs);    float Mask = 0.f;#if defined(OUT_BASECOLOR)    Out.MRT[0] = float4(BaseColor, 1.f) * Opacity;#elif defined(OUT_BASECOLOR_NORMAL_SPECULAR)    float3 PackedNormal = PackNormal(Normal);    Out.MRT[0] = float4(BaseColor, 1.f) * Opacity;    Out.MRT[1] = float4(PackedNormal.xy, Mask, 1.f) * Opacity;    Out.MRT[2] = float4(Specular, Roughness, PackedNormal.z, 1.f) * Opacity;#elif defined(OUT_WORLDHEIGHT)    float PackedHeight = PackWorldHeight(WorldHeight, ResolvedView.RuntimeVirtualTexturePackHeight);    Out.MRT[0] = float4(PackedHeight, 0, 0, 1);#endif}
由代码可以看出,这里直接调用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. */void CopyBaseColorNormalSpecularPS(    in float4 InPosition : SV_POSITION,    in noperspective float2 InTexCoord : TEXCOORD0,    out float4 OutColor0 : SV_Target0,    out float4 OutColor1 : SV_Target1){    float3 BaseColor = RenderTexture0.SampleLevel(TextureSampler0, InTexCoord, 0).xyz;    float2 NormalXY = RenderTexture1.SampleLevel(TextureSampler1, InTexCoord, 0).xy;    float3 RoughnessSpecularNormalZ = RenderTexture2.SampleLevel(TextureSampler2, InTexCoord, 0).xyz;    RoughnessSpecularNormalZ.z = round(RoughnessSpecularNormalZ.z);    OutColor0 = float4(BaseColor, NormalXY.x);    OutColor1 = float4(RoughnessSpecularNormalZ, NormalXY.y);}
序号5:这一步很简单,只是调用图形API Copy命令将最终渲染生成的VT纹理数据Copy到VT Physical Texture的对应的Page(区域)上。
至此RVT的纹理数据生成完毕,接下来是更新PageTable数据。
1.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* Buffer = (uint8*)RHILockVertexBuffer(UpdateBuffer, 0, TotalNumUpdates * sizeof(FPageTableUpdate), RLM_WriteOnly);for (uint32 LayerIndex = 0u; LayerIndex < Description.NumPageTableLayers; ++LayerIndex){    for (uint32 Mip = 0; Mip < NumPageTableLevels; Mip++)    {        const uint32 NumUpdates = ExpandedUpdates[LayerIndex][Mip].Num();        if (NumUpdates)        {            size_t UploadSize = NumUpdates * sizeof(FPageTableUpdate);            FMemory::Memcpy(Buffer, ExpandedUpdates[LayerIndex][Mip].GetData(), UploadSize);            Buffer += UploadSize;        }    }}RHIUnlockVertexBuffer(UpdateBuffer);
更新好PageTable Buffer之后,创建RVT内容类型对应的格式的PageTable RenderTarget,遍历PageTable RenderTarget的每个Mip Level,直接调用FRHICommandList执行渲染操作,将PageTable Buffer中数据写入到这个RT中。渲染部分代码如下:
uint32 MipSize = PageTableSize;for (uint32 Mip = 0; Mip < NumPageTableLevels; Mip++){    const uint32 NumUpdates = ExpandedUpdates[LayerIndex][Mip].Num();    if (NumUpdates)    {        FRHIRenderPassInfo RPInfo(PageTableTarget.TargetableTexture, ERenderTargetActions::Load_Store, nullptr, Mip);        RHICmdList.BeginRenderPass(RPInfo, TEXT("PageTableUpdate"));        RHICmdList.SetViewport(0, 0, 0.0f, MipSize, MipSize, 1.0f);        FGraphicsPipelineStateInitializer GraphicsPSOInit;        RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit);        GraphicsPSOInit.BlendState = BlendStateRHI;        GraphicsPSOInit.RasterizerState = TStaticRasterizerState<>::GetRHI();        GraphicsPSOInit.DepthStencilState = TStaticDepthStencilState::GetRHI();        GraphicsPSOInit.PrimitiveType = PT_TriangleList;        GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GEmptyVertexDeclaration.VertexDeclarationRHI;        GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader();        GraphicsPSOInit.BoundShaderState.PixelShaderRHI = PixelShader.GetPixelShader();        SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit);        {            FRHIVertexShader* ShaderRHI = VertexShader.GetVertexShader();            SetShaderValue(RHICmdList, ShaderRHI, VertexShader->PageTableSize, PageTableSize);            SetShaderValue(RHICmdList, ShaderRHI, VertexShader->FirstUpdate, FirstUpdate);            SetShaderValue(RHICmdList, ShaderRHI, VertexShader->NumUpdates, NumUpdates);            SetSRVParameter(RHICmdList, ShaderRHI, VertexShader->UpdateBuffer, UpdateBufferSRV);        }        // needs to be the same on shader side (faster on NVIDIA and AMD)        uint32 QuadsPerInstance = 8;        RHICmdList.SetStreamSource(0, NULL, 0);        RHICmdList.DrawIndexedPrimitive(GQuadIndexBuffer.IndexBufferRHI, 0, 0, 32, 0, 2 * QuadsPerInstance, FMath::DivideAndRoundUp(NumUpdates, QuadsPerInstance));        RHICmdList.EndRenderPass();        ExpandedUpdates[LayerIndex][Mip].Reset();    }    FirstUpdate += NumUpdates;    MipSize >>= 1;}
值得一提的是,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    // We can assume pPage fits in 6 bits and pack the final output to 16 bits    const uint PageCoordinateBitCount = 6;#else    const uint PageCoordinateBitCount = 8;#endif...uint Page = vLevel;Page |= pPage.x << 4;Page |= pPage.y << (4 + PageCoordinateBitCount);
五、RVT的使用
VT的数据准备好之后,就是如何使用了。RVT的使用主要是通过在Material的Shader中,加入RVT相关材质节点来采样RVT来完成的。下面是在Material Editor中使用Runtime Virtual Texutre Sample节点生成的HLSL使用RVT部分的代码:
VTPageTableResult Local1 = TextureLoadVirtualPageTable(VIRTUALTEXTURE_PAGETABLE_0    , VTPageTableUniform_Unpack(Material.VTPackedPageTableUniform[0 * 2], Material.VTPackedPageTableUniform[0 * 2 + 1])    , Parameters.SvPosition.xy    , Parameters.VirtualTextureFeedback    , 0 + LIGHTMAP_VT_ENABLED    , VirtualTextureWorldToUV(GetWorldPosition(Parameters)        , Material.VectorExpressions[4].rgb        , Material.VectorExpressions[3].rgb        , Material.VectorExpressions[2].rgb)    , VTADDRESSMODE_WRAP    , VTADDRESSMODE_WRAP);MaterialFloat4 Local2 = TextureVirtualSample(Material.VirtualTexturePhysicalTable_0    , GetMaterialSharedSampler(Material.VirtualTexturePhysicalTable_0Sampler, View.SharedBilinearClampedSampler)    , Local1    , 0    , VTUniform_Unpack(Material.VTPackedUniform[0]));
这里主要调用了2个Shader函数:TextureLoadVirtualPageTable和TextureVirtualSample。
TextureLoadVirtualPageTable函数用于生成VTPageTableResult结构,这个结构包含了间接寻址的Page数据;TextureVirtualSample函数中使用这个结构执行真正的纹理采样工作。

六、TextureLoadVirtualPageTable
分析第一个函数TextureLoadVirtualPageTable,代码如下:
VTPageTableResult TextureLoadVirtualPageTable(Texture2D PageTable0,    float2 UV, float MipBias,    float2 SvPositionXY,    VTPageTableUniform PageTableUniform,    uint AddressU, uint AddressV){    VTPageTableResult Result = (VTPageTableResult)0.0f;    const float2 ScaledUV = UV * PageTableUniform.UVScale;    uint vLevel = 0u;#if PIXELSHADER    vLevel = TextureComputeVirtualMipLevel(Result, ddx(ScaledUV), ddy(ScaledUV), MipBias, SvPositionXY, PageTableUniform);#endif // PIXELSHADER    TextureLoadVirtualPageTableInternal(Result, PageTable0, ScaledUV, vLevel, PageTableUniform, AddressU, AddressV);    return Result;}
其中UV参数是RVT的UV坐标,那么如何获取VT的UV呢?答案在VirtualTextureWorldToUV函数中:
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的Mip Level,为了实现较好的混合效果,这里根据当前帧ID生成交错的随机Noise扰动Level,代码如下:
uint TextureComputeVirtualMipLevel(inout VTPageTableResult OutResult,    float2 dUVdx, float2 dUVdy, float MipBias,    float2 SvPositionXY,    VTPageTableUniform PageTableUniform){    OutResult.dUVdx = dUVdx * PageTableUniform.SizeInPages;    OutResult.dUVdy = dUVdy * PageTableUniform.SizeInPages;    const float Noise = InterleavedGradientNoise(SvPositionXY, View.StateFrameIndexMod8);    const float ComputedLevel = MipLevelAniso2D(OutResult.dUVdx, OutResult.dUVdy, PageTableUniform.MaxAnisoLog2) + MipBias + Noise * 0.5f - 0.25f;    return clamp(int(ComputedLevel) + int(PageTableUniform.vPageTableMipBias), 0, int(PageTableUniform.MaxLevel));}
TextureLoadVirtualPageTableInternal函数代码如下:
void TextureLoadVirtualPageTableInternal(inout VTPageTableResult OutResult,    Texture2D PageTable0,    float2 UV, uint vLevel,    VTPageTableUniform PageTableUniform,    uint AddressU, uint AddressV){    UV.x = ApplyAddressMode(UV.x, AddressU);    UV.y = ApplyAddressMode(UV.y, AddressV);    OutResult.UV = UV * PageTableUniform.SizeInPages;    const uint vPageX = (uint(OutResult.UV.x) + PageTableUniform.XOffsetInPages) >> vLevel;    const uint vPageY = (uint(OutResult.UV.y) + PageTableUniform.YOffsetInPages) >> vLevel;    OutResult.PageTableValue[0] = PageTable0.Load(int3(vPageX, vPageY, vLevel));    OutResult.PageTableValue[1] = uint4(0u, 0u, 0u, 0u);    // 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;    OutResult.PackedRequest |= vLevel << 24;}
这个函数主要2个作用,一是生成用于寻址VT Physical Texture的PageTableValue,另一个是生成Feedback Request数据,具体有以下几个步骤:
  • 根据UV寻址模式修正虚拟纹理坐标。
  • 根据当前VT的Page数量和上一步修正过的虚拟纹理坐标计算出VT坐标对应的Page坐标。
  • 通过Page坐标加上Page的XY偏移,再根据Mip Level,计算出PageTable Texture的UV坐标,然后使用这个UV坐标和Mip Level采样PageTable Texture得到在Physical Texture上的信息,保存在PageTableValue中,在接下来的流程中使用。
  • 将第3步计算好的PageTable Texture的Page坐标和Mip Level保存在VTPageTableResult中,最后通过StoreVirtualTextureFeedback函数写入到VT Feedback Buffer中。

七、TextureVirtualSample
采样所需的VTPageTableResult数据准备完毕,在TextureVirtualSample函数中就是执行真正的Physical Texture采样逻辑,代码如下:
MaterialFloat4 TextureVirtualSample(    Texture2D Physical, SamplerState PhysicalSampler,    VTPageTableResult PageTableResult, uint LayerIndex,    VTUniform Uniform){    const float2 pUV = VTComputePhysicalUVs(PageTableResult, LayerIndex, Uniform);    return Physical.SampleGrad(PhysicalSampler, pUV, PageTableResult.dUVdx, PageTableResult.dUVdy); }
这个函数很简单,只有2个函数调用,第一行VTComputePhysicalUVs用于生成Physical Texture UV坐标,第二行用于执行渐变采样,所以这里重点是如何生成Physical Texture UV坐标,VTComputePhysicalUVs函数代码如下:
float2 VTComputePhysicalUVs(in out VTPageTableResult PageTableResult, uint LayerIndex, VTUniform Uniform){    const uint PackedPageTableValue = PageTableResult.PageTableValue[LayerIndex / 4u][LayerIndex & 3u];    // See packing in PageTableUpdate.usf    const uint vLevel = PackedPageTableValue & 0xf;    const float UVScale = 1.0f / (float)(1 << vLevel);    const float pPageX = (float)((PackedPageTableValue >> 4) & ((1 << Uniform.PageCoordinateBitCount) - 1));    const float pPageY = (float)(PackedPageTableValue >> (4 + Uniform.PageCoordinateBitCount));    const float2 vPageFrac = frac(PageTableResult.UV * UVScale);    const float2 pUV = float2(pPageX, pPageY) * Uniform.pPageSize + (vPageFrac * Uniform.vPageSize + Uniform.vPageBorderSize);    const float ddxyScale = UVScale * Uniform.vPageSize;    PageTableResult.dUVdx *= ddxyScale;    PageTableResult.dUVdy *= ddxyScale;    return pUV;}
这个函数通过在TextureLoadVirtualPageTableInternal中采样PageTable Texture得到在Physical Texture上的信息PackedPageTableValue,计算出采样Physical Texture的Mip Level和UV坐标,步骤如下:
  • PackedPageTableValue低4bit得到Mip Level,最多16级(0~15)。
  • 由Mip Level计算UV Scale。
  • 根据PackedPageTableValue的中高8位(32bit PageTable Texture)或6位(16bit PageTable Texture)计算出在Physical Texture的Page坐标。
  • 根据第2步的VU Scale计算出Mip Level对应的UV坐标,取小数部分UV坐标既是在Page内的UV坐标。
  • 根据第3步的Page坐标乘以每Page像素大小在整个Physical Texture像素大小的比值的倒数,得到Page在Physical Texture的起始UV坐标,加上把第4步的Page内UV缩放到整个Physical Texture UV坐标,再加上Page Border与Physical Texture像素比值的倒数,就得出最终的Physical Texture UV采样的坐标。
  • 将第2步的UV Scale缩放到整个Physical Texture,来计算最终在Physical Texture的XY方向的导数,作为最终采样时使用。

八、UE4 RVT资源格式及布局
RVT的布局配置中选择不同的虚拟纹理内容,将决定VT Physical Texture和PageTable Texture的像素格式和布局。
1.jpg
RVT 的布局配置中虚拟纹理内容选项

在代码中根据内容枚举类型返回Texture层数,如下所示:
int32 URuntimeVirtualTexture::GetLayerCount(ERuntimeVirtualTextureMaterialType InMaterialType){    switch (InMaterialType)    {    case ERuntimeVirtualTextureMaterialType::BaseColor:    case ERuntimeVirtualTextureMaterialType::WorldHeight:        return 1;    case ERuntimeVirtualTextureMaterialType::BaseColor_Normal_Specular:        return 2;    case ERuntimeVirtualTextureMaterialType::BaseColor_Normal_Specular_YCoCg:    case ERuntimeVirtualTextureMaterialType::BaseColor_Normal_Specular_Mask_YCoCg:        return 3;    default:        break;    }    // Implement logic for any missing material types    check(false);    return 1;}
如代码所示,虚拟纹理内容类型和层数之间对应关系如下:
  • 基础颜色或者场景高度,返回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坐标,总计16bit,否则使用32bit,其中4bit(0~15)Mip Level,以及8bit(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.cpp...static EPixelFormat GetFormatForNumLayers(uint32 NumLayers, EVTPageTableFormat Format){    const bool bUse16Bits = (Format == EVTPageTableFormat::UInt16);    switch (NumLayers)    {    case 1u: return bUse16Bits ? PF_R16_UINT : PF_R32_UINT;    case 2u: return bUse16Bits ? PF_R16G16_UINT : PF_R32G32_UINT;    case 3u:    case 4u: return bUse16Bits ? PF_R16G16B16A16_UINT : PF_R32G32B32A32_UINT;    default: checkNoEntry(); return PF_Unknown;    }}
十一、后记
在VT实现过程中,往往由于工程化程度不够而导致无法实用,UE4的VT使我们看到一个真正意义上工程化且实用的VT是如何实现的,分析它一方面对于更深入的了解UE4 VT有很好的帮助,另一方面对实现自己的VT系统也有很好的工程化参考意义,尤其是一些优化手段。最后需要说明的是,UE4的VT在实现过程中加入了大量的利于工程化的优化机制和手段,因此实际的代码要远比文中列出的庞杂,本文只是对关键部分的代码加以分析和说明,如果要了解更多的实现细节,可以按照文中梳理的脉络来阅读源码,相信会有更多收获。

4517.png

评分

参与人数 1活跃度 +15 展开 理由
KL呆呆L + 15 虽然看不懂 但很高大尚

查看全部评分

还没有设置签名!您可以在此展示你的链接,或者个人主页!
使用道具 <
您需要登录后才可以回帖 登录 | 注册

本版积分规则

快速回复 返回顶部 返回列表