延迟渲染
Posted PireannUE
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了延迟渲染相关的知识,希望对你有一定的参考价值。
基础概念
概念 |
缩写 |
中文名 |
解析 |
Render Target |
RT |
渲染目标/渲染纹理 |
一种位于GPU显存的纹理缓冲区,可用作颜色写入和混合目标 |
Geometry Buffer |
GBuffer |
几何缓冲区 |
一种特殊的RenderTarget,延迟着色技术的几何数据存储区 |
Rendering Path |
- |
渲染路径 |
将场景物体渲染到渲染纹理的过程中所使用的技术步骤 |
Forward Render |
- |
前向渲染 |
一种普通存在和天然支持的渲染路径 |
延迟渲染技术概述
延迟渲染技术的核心思想是将渲染分成两个Pass:几何Pass和光照Pass,目的是将计算量大的光照Pass延迟,让物体数量和光照数量解耦,以提升着色效率。以下是延迟渲染的流程图:
两个通道各自的作用:
几何通道
这个阶段将场景中所有不透明的物体和Masked物体用无光照材质渲染,然后将物体的几何信息写到GBuffer中。
其中几何信息由:物体位置或者深度,法线分布,材质信息(RGBA,镜面值等)
void RenderBasePass() SetupGBuffer(); // 设置几何数据缓冲区。 // 遍历场景的非半透明物体 for each(Object in OpaqueAndMaskedObjectsInScene) SetUnlitMaterial(Object); // 设置无光照的材质 DrawObjectToGBuffer(Object); // 渲染Object的几何信息到GBuffer,通常在GPU光栅化中完成。
光照通道
光照阶段利用几何通道阶段生成的GBuffer数据执行光照计算,它的核心逻辑是遍历渲染分辨率相同的像素,根据每个像素的uv坐标从GBuffer中采样获取该像素的几何信息,从而执行光照计算。伪代码如下:
void RenderLightingPass() BindGBuffer(); // 绑定几何数据缓冲区。 SetupRenderTarget(); // 设置渲染纹理。 // 遍历RT所有的像素 for each(pixel in RenderTargetPixels) // 获取GBuffer数据。 pixelData = GetPixelDataFromGBuffer(pixel.uv); // 清空累计颜色 color = 0; // 遍历所有灯光,将每个灯光的光照计算结果累加到颜色中。 for each(light in Lights) color += CalculateLightColor(light, pixelData); // 写入颜色到RT。 WriteColorToRenderTarget(color);
延迟渲染的优劣
优点:相较于前向渲染,延迟渲染的时间复杂度会低很多。做到了光照计算与物体解耦,只和RenderTarget相关
缺点:延迟渲染对MSAA的支持不太好,对Alpha Test的支持不太好
延迟渲染变种
1. Deferred Lighting(Light Pre-Pass)
需要三个Pass
第一个Pass是几何Pass:只输出每个像素光照计算需要的几何信息(法线,深度, PowSpecular)到GBuffer
第二个Pass是光照Pass:对GBuffer的数据取用,计算各个输入光源作用后的信息,存储这些信息到LBuffer
第三个Pass是Secondary Geometry Pass,LBuffer中的数据与DiffuseColor进行混合得到最红的输出Color
优点:相对延迟渲染,需要存储在GBuffer的信息急剧减少,可以较好地支持MSAA
缺点:需要渲染两边场景,增加了DrawCall
LBuffer
2. Tiled-Based Deferred Rendering(TBDR)
TBDR是基于瓦片的渲染,核心思想在于将渲染纹理分成规则的一个个四边形,然后利用四边形的包围盒剔除无用的光源,只保留有作用的光源,从而减少了实际光照计算中无效光源的计算量
具体流程如下:
(1) 将渲染纹理分成一个个均等面积的小块
(2) 根据Tile内的Depth范围计算出包围盒(视锥)
(3) 根据Tile的包围盒与Light的包围盒求交
(4) 抛弃不相交的Light,得到对该区域有作用的光源列表
(5) 遍历所有Tile,获得每个Tile的有作用的光源索引,计算该Tile内的光照信息
3. Clustered Deferred Rendering
分簇延迟渲染,相较于TBDR,分簇延迟渲染在不同深度进行了更加细粒度的划分
4. Decoupled Deferred Shading
解耦的延迟渲染,核心思想是增加compact geometry buffer用以存储着色采样数据(独立于可见性,避免存储了冗余数据和表面一致的或者shader计算结果之前存在的),通过memoization cache,压缩计算量,提升随机光栅化的着色重用率,降低抗锯齿的消耗。不同于之前将阴影样本存储在framebuffer中,可见性样本将对阴影样本的引用存储在紧凑的线性缓冲区中(memoizaiotn cache),以允许阴影重用。几何缓冲区的大小不随超采样密度而增长,而是由遮光率控制。
阴影样本信息的存储
5. Visibility Buffer
为了减少GBuffer占用,不渲染GBuffer,改成渲染Visibility Buffer。Visibility Buffer只存三角形和实例ID。在光照计算时分别从UAV和bindless texture里面读取真正需要的vertex attributes和贴图的属性,根据uv的差分自行计算mip-map
6. Fine Pruned Tiled Light Lists
对Tiled Shading的优化,由两个Pass来计算物体和光源求交
第一个Pass计算光源在屏幕空间的AABB,与物体的AABB进行初步判断
第二个Pass逐Tile和光源进行精确求交
7. Deferred Attribute Interpolation Shading
对内存的优化,不保存GBuffer,而是保存三角形信息,再通过插值计算,降低内存的消耗
第一个Pass,生成VisibilityPass
第二个Pass,采用Visibility,保证所有像素只有一个三角形写入memoization cache(记录三角形id和实际映射的缓冲区)
最后的Pass,计算每个三角形的屏幕空间的偏导数,以供插值使用
8. Deferred Coarse Pixel Shading
主要解决传统延迟渲染在屏幕空间的光照计算阶段高消耗的问题,在生成GBuffer时,额外生成ddx和ddy,再利用Compute Shader对某个大小的分块找到变化不明显的区域,使用低频着色(几个像素公用一次着色),从而提升着色效率,降低消耗。
9. Deferred Adaptive Compute Shading
核心思想在于将屏幕像素按照某种方式划分成5个Level的不同粒度的像素块,以便决定是直接从邻接level插值还是重新着色
示意图如下:
算法流程:
- 第一个Level,渲染1/16的像素
- 遍历2-5个Level,逐Level按照下面的步骤着色:
(1) 计算上一个Level相邻4个像素的相似度
(2) 若相似度小于某个阈值,认为此像素与周围像素表接近,当前像素可以通过周围像素插值获得
(3) 否则则开启着色计算
前向渲染及其变种
前向渲染
遍历场景中的所有物体,每个物体调用一次绘制指令,经由渲染管线光栅化之后显示在屏幕中。
// 遍历RT所有的像素 for each(object in ObjectsInScene) color = 0; // 遍历所有灯光,将每个灯光的光照计算结果累加到颜色中。 for each(light in Lights) color += CalculateLightColor(light, object); // 写入颜色到RT。 WriteColorToRenderTarget(color);
Forward+Rendering
相比传统的前向渲染,Forward+Rendering增加了光源剔除Pass。
DepthPrePass->LightCullingPass->RenderingPass
LightCullingPass和瓦片的延迟渲染类似,将屏幕划分成若干个Tile,将每个Tile和光源求交,有效光源写入Tile列表,以减少shader的光照计算量
存在的问题:由于街头视锥拉长之后在几何边界会有误差,可以使用SAT改善
Cluster Foward Rendering
将屏幕空间划分成均等Tile,深度细分成一个个簇
Volume Tiled Forward Rendering
步骤如下:
- 初始化阶段
(1) 计算Grid(Volume Tile)的尺寸,给定Tile
(2) 计算每个Volume Tile的AABB
- 更新阶段
(1) 深度PrePass,只记录半透明物体的深度
(2) 标记激活的Tile
(3) 创建和压缩Tile列表
(4) 将光源赋给Tile,每个线程执行一个激活的Tile,利用Tile的AABB和光源求交(可以用BVH优化),将相交的光源索引记录到对应的Tile的光源列表
(5) 着色
前向渲染和延迟渲染的对比
延迟渲染算法的比较
延迟着色场景渲染器
FSceneRenderer
FSceneRenderer用于处理和渲染场景,生成RHI层的渲染指令。FSceneRender由游戏线程FRenderModule::BeginRenderingViewFamily负责创建和初始化,并传递给渲染线程,渲染线程会调用FSceneRenderer::Render(),渲染完成后返回并删除FSceneRenderer实例。也就是说每帧FSceneRenderer实例都会被创建删除。
Tick中渲染的流程:计算ViewFamily、View的各种属性->发送渲染命令
发送渲染命令流程:创建场景渲染器->向场景渲染器发送渲染指令
向场景渲染器发送渲染指令流程:调用场景渲染器的Render接口->等待完成任务后删除实例
渲染器包括延迟渲染器(包含前向和延迟渲染)和移动端渲染器(移动端渲染器默认采用前向渲染)
FDeferredShadingSceneRenderer
FDeferredShadingSceneRenderer是UE在主机和PC端的渲染器,包含延迟渲染和前向渲染,本文主要介绍其延迟渲染路径
渲染器Render的阶段如下
阶段 |
解析 |
FScene::UpdateAllPrimitiveSceneInfos |
更新所有图元信息到GPU,若启用了GPUScene,将会用二维纹理或者StructBuffer来存储图元信息 |
FSceneRenderTarget::Allocate |
如果需要,重新分配场景的渲染纹理,以保存由足够大的内存去进行渲染 |
InitView |
采用裁剪等若干方式设置图元可视性,设置可见的动态阴影 |
PrePass/Depth only pass |
提前深度测试,用来渲染不透明物体的深度 |
Base pass |
几何通道。用来渲染不透明物体的几何信息,写道GBuffer中,此阶段不会计算动态光源的贡献,但是会计算Lightmap和天空的贡献 |
Issue Occlusion Queries / BeginOcclusionTest |
开启遮挡测试,此帧的渲染数据用于下一帧InitView阶段的遮挡剔除。可能会打包相近物体的包围盒来减少DrawCall |
Lighting |
光照通道,会计算开启阴影的光源的阴影图,并计算每个光源对屏幕空间的贡献,并累积到SceneColor中 |
Fog |
屏幕空间计算雾和大气对不透明物体表面像素的影响 |
Translucency |
渲染半透明物体阶段,由远到近逐个绘制离屏渲染纹理,接着用单个pass正确计算和混合光照结果 |
Post Processing |
后处理阶段。包含了不需要GBuffer的Bloom,色调映射,Gamma矫正等以及需要GBuffer的SSR, SSAO, SSGI等。此阶段会将半透明的渲染纹理混合到最终的场景中 |
可以用profilegpu来查看完整渲染过程
FScene::UpdateAllPrimitiveSceneInfos
主要作用是删除、增加、更新CPU侧的图元数据,且同步到GPU端。GPU存储结构有两种
I.每个图元独有一个UniformBuffer。在shader中访问UniformBuffer即可。
II.使用Texture2D或者StructedBuffer的GPUScene,所有图元按照数据规律存放,shader需要访问图元时需要从GPU的对应位置获取。以下时GPUScene的定义
class FGPUScene public: // 是否更新全部图元数据,通常用于调试,运行期会导致性能下降。 bool bUpdateAllPrimitives; // 需要更新数据的图元索引. TArray<int32> PrimitivesToUpdate; // 所有图元的bit,当对应索引的bit为1时表示需要更新(同时在PrimitivesToUpdate中). TBitArray<> PrimitivesMarkedToUpdate; // 存放图元的GPU数据结构, 可以是TextureBuffer或Texture2D, 但只有其中一种会被创建和生效, 移动端可由Mobile.UseGPUSceneTexture控制台变量设定. FRWBufferStructured PrimitiveBuffer; FTextureRWBuffer2D PrimitiveTexture; // 上传的buffer FScatterUploadBuffer PrimitiveUploadBuffer; FScatterUploadBuffer PrimitiveUploadViewBuffer; // 光照图 FGrowOnlySpanAllocator LightmapDataAllocator; FRWBufferStructured LightmapDataBuffer; FScatterUploadBuffer LightmapUploadBuffer; ; FScene::UpdateAllPrimitiveSceneInfos操作: 删除流程: example swap chain of removing X PrimitiveSceneProxies[0,0,0,6,X,6,6,6,2,2,2,2,1,1,1,7,4,8] PrimitiveSceneProxies[0,0,0,6,6,6,6,6,X,2,2,2,1,1,1,7,4,8] PrimitiveSceneProxies[0,0,0,6,6,6,6,6,2,2,2,X,1,1,1,7,4,8] PrimitiveSceneProxies[0,0,0,6,6,6,6,6,2,2,2,1,1,1,X,7,4,8] PrimitiveSceneProxies[0,0,0,6,6,6,6,6,2,2,2,1,1,1,7,X,4,8] PrimitiveSceneProxies[0,0,0,6,6,6,6,6,2,2,2,1,1,1,7,4,X,8] PrimitiveSceneProxies[0,0,0,6,6,6,6,6,2,2,2,1,1,1,7,4,8,X] 添加流程: example swap chain of inserting a type of 6 at the end PrimitiveSceneProxies[0,0,0,6,6,6,6,6,2,2,2,2,1,1,1,7,4,8,6] PrimitiveSceneProxies[0,0,0,6,6,6,6,6,6,2,2,2,1,1,1,7,4,8,2] PrimitiveSceneProxies[0,0,0,6,6,6,6,6,6,2,2,2,2,1,1,7,4,8,1] PrimitiveSceneProxies[0,0,0,6,6,6,6,6,6,2,2,2,2,1,1,1,4,8,7] PrimitiveSceneProxies[0,0,0,6,6,6,6,6,6,2,2,2,2,1,1,1,7,8,4] PrimitiveSceneProxies[0,0,0,6,6,6,6,6,6,2,2,2,2,1,1,1,7,4,8] 变化流程: MVP
对于所有被增加或者删除或者更改了数据的图元索引,会调用AddPrimitiveToUpdateGPU将它们放到待更新列表,GPUScene的PrimitiveToUpdate和PrimitivesMarkedToUpdate收集好所有要更新的图元索引后,会在InitView阶段同步给GPU,流程为:更新GPUScene资源->初始化View数据->同步CPU的GPUScene到GPU
同步的逻辑:
根据不同的GPUScene存储类型调用不同的接口(UpdateGPUSceneInternal)->判断图元信息是否同步->获取GPU的镜像资源(GPUScene.PrimitiveBuffer或者GPUScene.PrimitiveTexture, 如果资源不足就扩展,扩展资源会创建新资源然后将旧的拷贝到新的)->将所有需要更新的数据收集到PrimitiveUploadBuffer中,如果超过了Buffer的存储上限,会分批加载->收集完成后将UAV状态转换到Compute状态,以便上传数据时GPU拷贝数据->上传数据到GPU(Scene.GPUScene.PrimitiveUploadBuffer.ResourceUploadTo)->上传完以后将UAV状态转换到Gfx,以便渲染物体时shader可用->使镜像资源生效
InitView
主要负责可见性判断,收集场景图元数据和标记,创建可见网格命令,初始化Pass渲染所需要的数据等
流程:创建可见性帧设置预备阶段->特效系统初始化->创建可见性网格指令->计算可见性->胶囊阴影->初始化大气效果->创建可见性帧设置后置阶段->由GDoInitViewLightingAfterPrePass决定是否在PrePass之后初始化View->初始化view 的所有uniform buffer和RHI资源->计算体积雾->发送开始渲染事件
其中调用了很多接口:
PreVisibilityFrameSetup
设置场景中哪些需要渲染的(修改标记位),调用FSceneRenderer::PreVisibilityFrameSetup
做了大量初始化的工作,如静态网格,Groom,SkinCache,特效,TAA,ViewState等
ComputeViewVisibility
ComputeViewVisibility用于计算可见性
流程:分配可见光源列表->更新不判断可见性的静态网格->初始化所有View数据->创建光源信息->获取并解压上一帧的数据->冻结可见性->裁剪->处理隐蔽物体->处理静态场景->处理判断可见性的静态网格->收集所有View的动态网格->创建每个网格的MeshPass数据
粗略地预计算可见性:计算哈希值和桶索引,绘制可见性物体的包围盒,通过包围盒和视点去计算是否可见
PostVisibilityFrameSetup
主要防止视线外或者屏幕占比很小或者没有光照强度的光源进入shader计算
流程:处理贴花排序和调整历史RenderTarget->处理光源可见性,遍历所有光源,结合View的视锥判断光源的可见性(平行光源不需要裁剪,只有局部光源需要裁剪,判断View的视锥是否和光源包围盒相交,如果相交,视锥需要针对最大距离进行矫正,剔除距离较远的光源)
InitRHIResources
流程:创建和设置缓存视图的UniformBuffer->创建和设置视图的UniformBuffer->重置缓存的UniformBuffer->初始化透明体积光照参数
OnStartRender
流程:场景MRT的初始化->通知ViewState初始化->初始化和设置体积光传输体积->分配软件级场景遮挡剔除(针对不支持硬件剔除的低端机)
综上所述InitView的流程分为:
- PreVisibilityFrameSetup:可见性判断预处理阶段,主要是初始化和设置静态网格、Groom、SkinCache、特效、TAA、ViewState等
- 初始化特效系统
- ComputeViewVisibility:计算视图相关的可视性
(1) FPrimitiveSceneInfo::UpdateStaticsMeshes:更新静态网格数据
(2) ViewState::GetPrecomputedVisibilityData:获得可视性预计算数据
(3) FrustumCull:视锥体剔除
(4) ComputeAndMarkRelevanceForViewParallel:计算和标记视图并行处理的关联数据
(5) GatherDynamicMeshElements:收集view中的动态可见元素
(6) SetupMeshPass:设置网格Pass数据,将FMeshBatch转化为FMeshDrawsCommand
- CreateIndirectCapsuleShadows:创建胶囊阴影
- UpdateSkyIrradianceGPUBuffer:更新天空环境光照GPU数据
- InitSkyAtmosphereForViews:初始化大气效果
- PostVisibilityFrameSetup:可见性判断后置阶段,利用视锥剔除影响较小或者无影响的光照信息
- View.InitRHIResources:初始化视图的部分RHI资源
- SetupVolumetricFrog:初始化体积雾
- OnStartRender:通知RHI开启渲染
PrePass
提前深度测试,可以由DBuffer或者ForwadingShading触发,通常用建立Hierarchial-Z,以便开启硬件的Early-Z技术
RenderPrePass的整体逻辑流程如下:
非并行模式(等待之前任务结束),并行模式(分配深度缓冲),每个View都需要去绘制一遍深度缓冲(创建和设置UniformBuffer->处理渲染状态)
PrePass是禁止写入颜色,开启深度测试和写入,深度比较函数是接近或者相等
WorldGridMaterial中的Defult Lit中存在冗余节点。可以在配置文件中修改,以提升效率
深度绘制的模式有四种:
- DDM_Node:不绘制深度
- DDM_NonMaskedOnly:只绘制Opaque材质
- DDM_AllOcculders:Opaque和Masked材质,但是不包含关闭了bUseAsOccluder的物体
- DDM_AllOpaque:全部不透明物体模式,所有物体需绘制,且每个像素都要匹配BasePass的深度
- DDM_MaskedOnly:只绘制Masked材质
而深度绘制的模式由“Engine\\Source\\Runtime\\Renderer\\Private\\RendererScene.cpp”中的UpdateEarlyZPassMode()中设置,通过命令行重写,也可以由工程设置中指定
Tips:DBuffer贴图和模板LOD抖动强制全部模式
BasePass
延迟渲染中的几何通道,通过GBuffer来存储几何信息
流程:绘制场景深度->深度纹理解析(只有开启了MSAA才会被执行)->光源格子化,常用于Cluster光照计算中快速剔除光源->分配场景GBuffer->渲染BasePass
渲染BasePass需要遍历所有View去渲染BasePass
BasePass渲染状态
void SetupBasePassState(FExclusiveDepthStencil::Type BasePassDepthStencilAccess, const bool bShaderComplexity, FMeshPassProcessorRenderState& DrawRenderState) DrawRenderState.SetDepthStencilAccess(BasePassDepthStencilAccess); (......) // 所有GBuffer都开启了混合. static const auto CVar = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.BasePassOutputsVelocityDebug")); if (CVar && CVar->GetValueOnRenderThread() == 2) DrawRenderState.SetBlendState(TStaticBlendStateWriteMask<CW_RGBA, CW_RGBA, CW_RGBA, CW_RGBA, CW_RGBA, CW_RGBA, CW_NONE>::GetRHI()); else DrawRenderState.SetBlendState(TStaticBlendStateWriteMask<CW_RGBA, CW_RGBA, CW_RGBA, CW_RGBA>::GetRHI()); // 开启了深度写入和测试, 比较函数为NearOrEqual. if (DrawRenderState.GetDepthStencilAccess() & FExclusiveDepthStencil::DepthWrite) DrawRenderState.SetDepthStencilState(TStaticDepthStencilState<true, CF_DepthNearOrEqual>::GetRHI()); else DrawRenderState.SetDepthStencilState(TStaticDepthStencilState<false, CF_DepthNearOrEqual>::GetRHI()); BasePass渲染材质 void FBasePassMeshProcessor::AddMeshBatch(const FMeshBatch& RESTRICT MeshBatch, uint64 BatchElementMask, const FPrimitiveSceneProxy* RESTRICT PrimitiveSceneProxy, int32 StaticMeshId) if (MeshBatch.bUseForMaterial) const FMaterialRenderProxy* FallbackMaterialRenderProxyPtr = nullptr; const FMaterial& Material = MeshBatch.MaterialRenderProxy->GetMaterialWithFallback(FeatureLevel, FallbackMaterialRenderProxyPtr); (.....)
由此可见,BasePass一般情况下也是使用FMeshBatch收集到的材质,唯一不同的是shader中不开启光照计算
所以伪代码如下:
foreach(scene in scenes)
foreach(view in views)
foreach(mesh in meshes)
DrawMesh(...) // 每次调用渲染就执行一次BasePassVertexShader和BasePassPixelShader的代码.
LightingPass
光照通道,此阶段会计算开启阴影的光源的阴影图,也会计算每个灯光对屏幕空间像素的贡献量,累计到SceneColor中,此外也会计算光源对Transluency lighting volumes的贡献值。
流程:渲染非直接漫反射和AO->渲染非直接胶囊阴影->会修改从BasePass输出的已经添加了非直接光源的场景颜色->渲染距离长AO->清理光照体素->渲染光源->添加CubeMap的透明体素光照->过滤透明体素光照->光照组合预备阶段(如LPV)->渲染天空漫反射和只作用于不透明物体的反射->解析场景颜色(把场景颜色的RT变为RSV)->计算SSS效果
LightingPass会先绘制无阴影光照,起始索引由SortedLightSet.SimpleLightEnd决定;再绘制带阴影的光照,起始索引由SortedLightSet.AttenuaionLightStart决定,无阴影的光源才支持Tiled和Clustered。
渲染光源的代码再RenderLights中,流程如下:
RenderLights
- 设置混合状态为叠加,以便所有光源的光照强度叠加到同一张纹理中
- 遍历所有View,将此光源给每个View都绘制一遍
- 判断是否为平行光
(1) 若为平行光
① 设置延迟光源的shader和pso参数
② 使用全屏范围的矩形来绘制
(2) 若为局部光源
① 决定是否开启深度包围盒测试
② 设置延迟光源的shader和pso参数
③ 深度包围盒测试只在带阴影的光源中有效,可以有效剔除在深度范围之外的像素(UE中使用了逆反的深度所有far<near)
④ 点光源用球体绘制,聚光灯用锥体绘制
伪代码如下:
foreach(scene in scenes)
foreach(light in lights)
foreach(view in views)
RenderLight() // 每次调用渲染光源就执行一遍DeferredLightVertexShaders和DeferredLightPixelShaders的代码.
Translucency
渲染半透明物体的阶段,所有半透明物体是由远到近渲染的,接着用单独的pass以正确计算和混合光照效果。在该阶段会渲染半透明的颜色、扰动纹理(用于折射)、速度缓冲(用于TAA)
渲染半透明物体的代码逻辑在RenderTranslucency中
RenderTranslucency
// Source\\Runtime\\Renderer\\Private\\TranslucentRendering.cpp void FDeferredShadingSceneRenderer::RenderTranslucency(FRHICommandListImmediate& RHICmdList, bool bDrawUnderwaterViews) TRefCountPtr<IPooledRenderTarget> SceneColorCopy; if (!bDrawUnderwaterViews) ConditionalResolveSceneColorForTranslucentMaterials(RHICmdList, SceneColorCopy); // Disable UAV cache flushing so we have optimal VT feedback performance. RHICmdList.BeginUAVOverlap(); // 在景深之后渲染半透明物体。 if (ViewFamily.AllowTranslucencyAfterDOF()) // 第一个Pass渲染标准的半透明物体。 RenderTranslucencyInner(RHICmdList, ETranslucencyPass::TPT_StandardTranslucency, SceneColorCopy, bDrawUnderwaterViews); // 第二个Pass渲染DOF之后的半透明物体, 会存储在单独的一张半透明RT中, 以便稍后使用. RenderTranslucencyInner(RHICmdList, ETranslucencyPass::TPT_TranslucencyAfterDOF, SceneColorCopy, bDrawUnderwaterViews); // 第三个Pass将半透明的RT和场景颜色缓冲在DOF pass之后混合起来. RenderTranslucencyInner(RHICmdList, ETranslucencyPass::TPT_TranslucencyAfterDOFModulate, SceneColorCopy, bDrawUnderwaterViews); else // 普通模式, 单个Pass即渲染完所有的半透明物体. RenderTranslucencyInner(RHICmdList, ETranslucencyPass::TPT_AllTranslucency, SceneColorCopy, bDrawUnderwaterViews); RHICmdList.EndUAVOverlap();
如果需要在景深之后渲染半透明物体,需要经历三个Pass
1.渲染标准半透明物体
2.渲染景深之后的半透明物体,会存储在一张半透明RenderTarget中
3.将半透明RenderTarget和场景颜色缓冲在景深之后混合起来
否则则在单个Pass中由远到近渲染半透明物体(RenderTranslucencyInner)
RenderTranslucencyInner
对所有的半透明物体进行渲染,与BasePass类似,只不过只存储半透明物体的UniformBuffer,以及设置半透明物体渲染的RenderState。并且光照计算上有区别。
半透明物体渲染处于分离队列中(在工程的Render中设置,separate Translucency)
如果要将透明物体加入分离队列,只需要将其使用的材质开启Render After DOF
PostProcessing
后处理没有直接使用RHICommandList,而是用GraphBuilder代替。GraphBuilder是依赖性渲染图,可以自动裁剪无用的Pass,自动管理Pass之间的依赖性和资源的生命周期,以及资源、PSO、渲染指令的优化
渲染依赖图系统
运行在pass建立之后。并非立即执行pass,而是延迟到整个帧已记录到依赖图标数据结构之中后再执行。当完成了对所有pass的收集之后,会执行各类裁剪和优化,可以自动裁剪无用的pass,再按照依赖性的排序顺序进行编译和执行。这样可以做到上层渲染逻辑和下层资源隔离
帧依赖图关系:
AddPostProcessingPasses
// 后处理的所有Pass. enum class EPass : uint32 MotionBlur, Tonemap, //色调映射 FXAA, PostProcessMaterialAfterTonemapping, VisualizeDepthOfField, VisualizeStationaryLightOverlap, VisualizeLightCulling, SelectionOutline, EditorPrimitive, VisualizeShadingModels, VisualizeGBufferHints, VisualizeSubsurface, VisualizeGBufferOverview, VisualizeHDR, PixelInspector, HMDDistortion, HighResolutionScreenshotMask, PrimaryUpscale, SecondaryUpscale, MAX ; // 后处理的所有Pass对应的名字. const TCHAR* PassNames[] = TEXT("MotionBlur"), TEXT("Tonemap"), TEXT("FXAA"), TEXT("PostProcessMaterial (AfterTonemapping)"), TEXT("VisualizeDepthOfField"), TEXT("VisualizeStationaryLightOverlap"), TEXT("VisualizeLightCulling"), TEXT("SelectionOutline"), TEXT("EditorPrimitive"), TEXT("VisualizeShadingModels"), TEXT("VisualizeGBufferHints"), TEXT("VisualizeSubsurface"), TEXT("VisualizeGBufferOverview"), TEXT("VisualizeHDR"), TEXT("PixelInspector"), TEXT("HMDDistortion"), TEXT("HighResolutionScreenshotMask"), TEXT("PrimaryUpscale"), TEXT("SecondaryUpscale") ;
用来增加后处理通道
流程如下:初始化数据和标记->根据标记开启或者关闭对应的后处理通道->后处理(
- 处理MotionBlur, Tonemap, PostProcessMaterialAfterTonemapping等:获取材质输入->处理各类标记->开启或者关闭特殊后处理->半透明混合进场景颜色RT前的后处理->色调映射前的后处理->TAA
- SSR: 材质输入后处理材质-SSR输入/运动模糊/泛光->Bloom是否有效->色调映射->FXAA->颜色映射后的处理材质
- 组合分离队列的半透明RT和伽马矫正
)
后处理渲染分为以下几个阶段:
BL_AfterTonemapping, // 色调映射之后.
BL_BeforeTonemapping, // 色调映射之前.
BL_BeforeTranslucency, // 半透明组合之前.
BL_ReplacingTonemapper, // 替换掉色调映射.
BL_SSRInput, // SSR输入.
BL_MAX,
默认混合阶段是在色调映射之后,以下为添加色调映射为例,讲解如果向渲染器添加后处理
// Engine\\Source\\Runtime\\Renderer\\Private\\PostProcess\\PostProcessing.cpp static FRCPassPostProcessTonemap* AddTonemapper( FPostprocessContext& Context, const FRenderingCompositeOutputRef& BloomOutputCombined, const FRenderingCompositeOutputRef& EyeAdaptation, const EAutoExposureMethod& EyeAdapationMethodId, const bool bDoGammaOnly, const bool bHDRTonemapperOutput, const bool bMetalMSAAHDRDecode, const bool bIsMobileDof) const FViewInfo& View = Context.View; const EStereoscopicPass StereoPass = View.StereoPass; FRenderingCompositeOutputRef TonemapperCombinedLUTOutputRef; // LUT Pass. if (IStereoRendering::IsAPrimaryView(View)) TonemapperCombinedLUTOutputRef = AddCombineLUTPass(Context.Graph); const bool bDoEyeAdaptation = IsAutoExposureMethodSupported(View.GetFeatureLevel(), EyeAdapationMethodId) && EyeAdaptation.IsValid(); // 向Context.Graph注册一个Pass, 对象类型是FRCPassPostProcessTonemap. FRCPassPostProcessTonemap* PostProcessTonemap = Context.Graph.RegisterPass(new(FMemStack::Get()) FRCPassPostProcessTonemap(bDoGammaOnly, bDoEyeAdaptation, bHDRTonemapperOutput, bMetalMSAAHDRDecode, bIsMobileDof)); // 设置输入纹理. PostProcessTonemap->SetInput(ePId_Input0, Context.FinalOutput); PostProcessTonemap->SetInput(ePId_Input1, BloomOutputCombined); PostProcessTonemap->SetInput(ePId_Input2, EyeAdaptation); PostProcessTonemap->SetInput(ePId_Input3, TonemapperCombinedLUTOutputRef); // 设置输出. Context.FinalOutput = FRenderingCompositeOutputRef(PostProcessTonemap); return PostProcessTonemap;
Metal2剖析:传统延迟渲染和TBDR
针对延迟渲染,官方给出了一个Demo,分别实现了传统的双Pass延迟渲染和利用Metal的特性实现的单一Pass延迟渲染。单Pass延迟渲染主要依靠iOS和tvOS平台的Tile based特性来实现。这篇文章主要根据官方的延迟渲染Demo来分析两种延迟渲染的原理区别,扩展Programmable blending特性实现延迟渲染和ImageBlock实现延迟渲染进行优化的原理,挖掘总结用到的Metal引擎特性和相关知识点。
官方Metal Demo列表地址:https://developer.apple.com/metal/sample-code/
延迟渲染Demo地址:https://developer.apple.com/documentation/metal/deferred_lighting?language=objc
文章目录
以上是关于延迟渲染的主要内容,如果未能解决你的问题,请参考以下文章