您需要 登录 才可以下载或查看,没有账号?注册
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运行时采样阶段。 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的像素格式和布局。
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在实现过程中加入了大量的利于工程化的优化机制和手段,因此实际的代码要远比文中列出的庞杂,本文只是对关键部分的代码加以分析和说明,如果要了解更多的实现细节,可以按照文中梳理的脉络来阅读源码,相信会有更多收获。
|