UE4的渲染流程

虚幻引擎在FEngineLoop::PreInit中对渲染线程进行初始化。

渲染线程的启动位于StartRenderingThread全局函数中。

  1. 创建渲染线程类实例
  2. 通过FRunnableThread::Create函数创建渲染线程
  3. 等待渲染线程准备好,从TaskGraph取出任务并执行
  4. 注册渲染线程
  5. 创建渲染线程心跳更新线程

渲染线程的主要执行在全局函数RenderingThreadMain中,游戏线程会借助EQUEUE_Render_COMMAND宏,向渲染线程的TaskMap中添加渲染任务。渲染线程则不断的提取这些任务去执行。

这里需要注意,渲染线程并非直接向GPU发送指令,而是将渲染命令添加到RHICommandList,也就是RHI命令列表中。由RHI线程不断取出指令,向GPU发送。并阻塞等待结果。此时,RHI线程虽然阻塞,但是渲染线程依然正常工作,可以继续处理向RHI命令列表填充指令,从而增加CPU时间的利用率,避免渲染线程凭空等待GPU的处理。

 

渲染架构

虚幻引擎对于场景中所有不透明物体的渲染方式,是延迟渲染

对于半透明物体的渲染方式,是前向渲染

 

在虚幻引擎中,先进行延迟光照计算不透明物体,然后借助深度排序,计算半透明物体。

FDeferedSceneRender::Render函数

 

1、初始化视口 InitViews

进行必要的可见性剔除。分为三步:预设置可见性,可见性计算,完成可见性计算。

 

预设置可见性 PreVisibilityFrameSetup

1.根据当前画质,设置TemporalAA的采样方式,同时确定采样位置。采样位置用来微调视口矩阵。TemporalAA采样,每一帧渲染的时候,让这个像素覆盖的位置进行微弱的偏移,然后混合前面几帧的渲染结果。

2.设置视口矩阵,包括视口投影矩阵和转换矩阵。

 

可见性计算 ComputeViewVisibility

1.初始化用于可视化检测的缓冲区,位数组,用0和1表示是否可见。翻译为位图BitMap

2.视椎体剔除,对应函数FrustumCull,该函数内部使用ParallelFor函数的线性剔除,进行并行化的异步剔除。

3.遮挡剔除

4.根据可见性位图,设置每个需要渲染对象的可见性,即Hidden flags

5.开发者控制对象可见

6.获取所有对象的渲染信息,对应函数是每个RenderProxy的GetDynamicMeshElements函数。

 

网格物体组件对应的容器是RenderProxy,材质对象的容器是MaterialRenderProxy

 

完成可见性计算 PostVisibilityFrameSetup

1.对半透明的对象进行排序。半透明对象的渲染由于涉及互相遮挡,必须按照从后往前的顺序来渲染。

2.对每个光照确定当前光照可见的对象列表

3.初始化雾与大气的常量值。

4.完成对阴影的计算。包括对覆盖整个世界的阴影,对固定光照的级联阴影贴图和对逐对象的阴影贴图的计算。

 

虚幻引擎的剔除方式是借助ParallelFor的线性剔除,并行化的线性结构剔除在性能上优于基于树的剔除。

 

2、PrePass 预处理阶段

降低Base Pass的渲染工作量。通过渲染一次深度信息,如果某个像素点的深度不符合要求,这个像素点就不会进行工作量最大的像素渲染器计算。

不是基于分块的GPU,渲染器的EarlyZPassMode参数不为DDM_None,或GEarlyZPassMovable不为0,才会进行PrePass计算。

 

对象的渲染按照设置渲染状态,载入着色器,设置渲染参数,提交渲染请求,写入渲染目标缓冲区的步骤进行。

 

设置渲染状态 SetupPrePassView

关闭颜色写入,打开深度测试与深度写入。PrePass不需要计算颜色,只需要计算每个不透明物体像素的深度。

 

渲染静态数据

三个绘制列表由静态模型组成,通过可见性位图控制是否可见。

  1. 只绘制深度的PositionOnlyDepthDrawList
  2. 主要绘制不透明物体的DepthDrawList
  3. 带蒙版的深度绘制列表MaskedDepthDrawList,蒙版对应材质系统中的Mask类型

渲染动态数据

通过ShouldUseAsOccluder函数询问Render Proxy是否被当做一个遮挡物体,是否为可移动,决定是否需要在这个阶段绘制。

 

写入渲染目标缓冲区

通过RHI的SetRenderTarget设置。

 

TStaticMeshDrawList::DrawVisible函数

绘制可见对象

绘制可见对象的基础是可见对象列表,在绘制之前,每个绘制列表已经进行了排序,尽可能共用同样的绘制状态。

每个绘制列表都共用以下着色器状态,区别只是在于具体参数不同:

  1. 顶点描述 Vertex Declaration
  2. 顶点着色器 Vertex Shader
  3. 壳着色器 Hull Shader
  4. 域着色器 Domain Shader
  5. 像素着色器 Pixel Shader
  6. 几何着色器 Geometry Shader

 

载入公共着色器的信息 SetBoundShaderState 和SetSharedState

SetBoundShaderState 载入需要的着色器

SetSharedState 对于TBasePass,设置顶点着色器和像素着色器的参数。

 

逐元素渲染

1.对于每个DrawingPolicy调用SetMeshRenderState函数,设置渲染状态。包括调用每个着色器的SetMesh函数,以设置与当前Mesh相关的参数

2.调用Batch Element的DrawMesh函数,完成绘制。调用RHICmdList的DrawIndexedPrimitive函数,指定顶点缓冲区和索引缓冲区的位置。

 

3、BasePass

极为重要的阶段,通过对逐对象的绘制,将每个对象和光照相关的信息都写入到缓冲区中。

BasePass和PrePass的过程非常接近,分为设置渲染状态,渲染静态数据和渲染动态数据。

 

设置渲染状态

1.如果PrePass已经写入深度,则深度写入被关闭,直接使用已经写入的深度结果。

2.通过RHICmdList.SetBlendState,打开前4个渲染目标的RGBA写入。TStaticBlendStateWriteMask用模板参数定义渲染目标是否可写入,最高支持8个渲染目标。

RHICmdList.SetBlendState(TStaticBlendStateWriteMask<CW_RGBA, CW_RGBA, CW_RGBA, CW_RGBA>::GetRHI());

3.设置视口区域大小。这个大小会因为是否开启InstancedStereoPass而有所变化。

 

渲染静态数据

如果PrePass已经进行深度渲染,那么会先渲染Masked蒙版对象,然后渲染普通不透明对象。否则,先渲染不透明对象,再渲染蒙版对象。

 

渲染动态数据

与PrePass基本相同

 

BasePass采用MRT(Multi_Render Target)多渲染目标技术,从而允许Shader在渲染过程中向多个渲染目标进行输出。

渲染目标来自哪里?

渲染目标由当前请求渲染的视口(Viewport)分配,对应FSceneViewport::BeginRenderFramw函数。

如何写入?输出到何处?

并没有在C++代码中,而是在Shader着色器代码中。打开Engine/Shader/BasePassPixelShader.usf文件,大体过程:

  • 通过GetMaterialXXX函数,获取材质的各个参数,比如BaseColor基本颜色,Metallic金属等。
  • 然后,填充到GBuffer结构体中
  • 最后,通过EncodeGBuffer函数,把GBuffer结构体压缩、编码后,输出到SV_Target。

 

 

RenderOccusion渲染遮挡

虚幻引擎的遮挡计算,实质上是在PrePass中直接进行基于并行队列的硬件遮挡查询。除非在r.HZBOcclusion这个控制台变量被设置为1的情况下,或者有些特效需要的情况下,才会开启Hierarchical Z-Buffer Occlusion Culling 作用遮档查询。

全平台默认关闭

总体来说,这个步骤是为了尽可能剔除处于屏幕内但是被其他对象遮挡的对象。在视口初始化阶段,剔除了处于视锥体之外的对象。但是依然有大量对象处于视锥体内,却被其他对象遮挡。比如一座山背面的一大堆石头,这些石头能够正常通过我们的视锥体遮挡测试,却并不需要渲染。

因此, HZB渲染遮挡技术被用于解决这个问题,通常的HZB步骤如下:

  • (1)预先准备屏幕的深度缓冲区,这个缓冲区将会作为深度测试的基础数据。因此,这个步骤必须在PrePass之后,如果没有PrePass,则必须在BasePass 之后。
  • (2)逐层创建缓冲区的Mipmap级联贴图。层级越高,贴图分辨率越低,对应的区域越大。而每个层级对应这个区域“最远”元素到屏幕的距离(深度最大值)。
  • (3)计算所有需要进行测试的对象的包围球半径,根据这个半径,选择对应的深度缓冲区层级进行深度测试,判断是否被遮挡。这个的用意在于,如果对象较大,我们可以直接用更高的层级进行测试,这个对象的深度若比这个层级对应的距离还远,那么该对象一定被遮挡,因为层级对应的是这一片区域中可见元素的最远距离。

需要注意的是, OpenGL平台下不会进行这个测试。这个步骤中的第二步可以使用像素着色器多次绘制完成级联贴图层级,第三步则可以使用计算着色器ComputeShader,或者使用顶点着色器进行计算,将结果写入到一个渲染目标中。从而借助GPU的高度并行化来加速这个遮挡剔除过程。

这个步骤输出的结果会被用于下一帧计算,而不是在本帧。

 

光照渲染

对应函数RenderLights,光照渲染与阴影渲染是分离的,阴影渲染是在视口初始化阶段完成的,光照渲染大体步骤如下:

  1. 收集可见光源。对可见性的判断,利用视口初始化阶段保存的VisibleLightInfos信息,以当前Id查询即可获得结果。对每个光源构建FLightSceneInfo结构,然后通过ShouldRenderLights对光源是否需要渲染进行计算。
  2. 对收集好的光源进行排序。将不需要投射阴影、无光照函数的光源排在前面。
  3. 如果是TiledDeferredLighting,则通过RenderTiledDeferredLighting对光照进行计算。如果是PC平台,使用RenderLight函数进行光照计算。
  4. 如果平台支持Shader Model 5,则会计算反射阴影贴图与PLV信息。

 

核心光照渲染RenderLight函数

每个光源都会调用这个函数,遍历所有视口,计算光照强度,并叠加到屏幕颜色上。

 

1. 设置混合模式为叠加

2. 判断光源类型

平行光源

  • 载入延迟渲染光照对应的顶点着色器(TDeferredLightVS)和像素着色器(TDeferredLightPS)
  • 设置光照参数
  • 绘制一个覆盖全屏幕的矩阵,调用着色器。

非平行光源

  • 判断摄像机是否在光源范围内
  • 如果是,关闭深度测试,从而避免背面被遮盖部分不进行光照渲染
  • 否则,打开深度测试,以加速渲染
  • 载入着色器
  • 设置光照参数
  • 根据是点光源还是聚光灯,绘制一个对应的几何体,从而排除几何体外对象的渲染,加速光照计算。

 

ShaderMap

顶点工厂:负责抽象顶点数据以供后面的着色器获取,从而让着色器忽略由于顶点类型造成的差异。

当前着色器继承自FMaterialShader,则对每个材质类型编译出一组对应渲染管线的着色器

当前着色器继承自FMeshMaterialShader,则对每个材质类型的每个顶点工厂类型编译出一组顶点着色器和像素着色器。

通过GetMaterialXXX,可以获取材质的参数。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值