ue渲染就第一帧有问题

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ue渲染就第一帧有问题相关的知识,希望对你有一定的参考价值。

使用RenderDoc在编辑器中捕捉了一帧。一个实际游戏里的渲染流程和这个可能不一样,但通过捕捉到的数据我们可以粗略地窥见UE4是怎么渲染一帧的。

声明:以下分析基于GPU捕捉数据以及UE 4.17.1的渲染器代码,(作者)本人先前没有使用UE的经验。如果我漏掉了什么东西,请在评论中让我知晓。

幸运的是UE4的draw call列表非常整洁,并且有良好的注释,这会使我们分析起来更简单。如果你的场景中缺了些材质或者你的渲染质量设置得较低,你捕捉到的draw call列表可能和我的不一样。例如如果你的场景中没有粒子效果,那么ParticalSimulation这个 render pass就不会出现。

SlateUI这个pass包括了所有UE编辑器用于渲染UI的渲染调用,这一部分本文将会忽略,重点关注Scene下的所有render pass。

粒子模拟
UE4的一帧以ParticleSimulation pass开始。这一步在GPU上计算了场景里所有的粒子发射器(emitter)的粒子运动以及其他属性,并将结果输出到两个渲染目标(rendertarget)上,一个格式为RGBA32_Float,保存位置,另一个为RGBA16_Float,保存速度以及其他一些和粒子时间/生存周期相关的数据。下图展示了RGBA32_Float格式的渲染目标保存的数据,每一个像素代表了一个sprite的世界坐标。

我在场景中添加的粒子效果似乎有两个emitter在GPU上模拟时不需要进行碰撞检测,所以可以在每一帧较早的时候运行其对应的render pass。

Z-Prepass
接下来就是PrePass流程,这一步其实就是z-prepass,将所有不透明物体渲染到一个R24G8的深度缓冲中。

值得注意的是UE4使用reverse-Z来保存深度,意味着近裁面的深度值为1,远裁面的深度值为0。这使得深度缓冲的精度更高,避免在远处发生z-fighting的现象。从该pass的名字可以看出这一步是由“DBuffer”触发的。DBuffer是UE4用来保存延迟贴花(deferred decal)的缓冲,这一步需要场景深度,所以会启动Z-prepass。这个Z-buffer还会用在其他地方,例如遮挡检测和屏幕空间反射,这些我们会在之后提及。

Draw call列表中的一些渲染pass似乎是空的,例如ResolveSceneDepth,这一步我猜测是用于那些需要在使用纹理前resolve渲染目标的平台(PC平台不需要);又比如ShadowFrustumQueries,这一步看起来是个傀儡标记,因为真正的阴影遮挡测试发生在写一个渲染pass中。

遮挡检测
BeginOcclusionTests负责一帧中所有遮挡测试。UE4默认使用硬件遮挡查询(hardware occlusion queries)来进行遮挡测试。简而言之分为3步:

将所有被标记为遮挡体的物体(例如一个较大的solid mesh)渲染进一个深度缓冲。
创建一个遮挡查询(occlusion query),提交该查询并渲染那些我们希望查询遮挡情况的模型。这一步使用硬件深度测试(z-test)以及我们在第一步中创建的深度缓冲。遮挡查询将返回通过深度测试的像素数量,如果结果是0就意味着该物体完全被solid mesh遮挡。由于为了深度测试而去把完整的模型渲染一遍的开销很高,这一步我们渲染模型的包围盒,而不是原模型,如果该包围盒不可见(也就是没通过深度测试),那么该包围盒所代表的模型肯定也不可见。
将查询结果读回CPU,根据被渲染像素的数量我们决定是否提交模型给GPU渲染(即便是有一小部分像素可见我们也可以不读渲染这个模型)。
UE4根据具体情况决定使用哪一类遮挡查询:

硬件遮挡查询有诸多劣势,例如有drawcall粒度上的问题,渲染器需要对每一个模型(或者一个模型批次)提交一个drawcall来进行遮挡查询,这会使得每一帧的drawcall数量显著上升;还有一个问题是硬件遮挡查询需要将结果读回到CPU,这就需要在CPU和GPU之间同步,并且要求CPU一直等待到GPU完成查询处理的时刻。这对instanced物体并不友好,但在这里我们先忽略这个问题。

对于CPU与GPU间的同步问题,UE4使用和其他引擎类似的方法:将CPU对数据的读回操作延迟几帧进行。这个方法大部分情况下可行,但在摄像机高速移动的时候可能会导致物体的突然出现(pop in)(实践中这不是个大问题,因为物体在遮挡剔除时使用包围盒来计算遮挡,这一步是保守,即便完全不可见的物体也可能被标记为可见)。额外的drawcall开销依然存在,但这个问题也是可以解决的。UE4通过以下方法来减轻这个问题的影响:

首先所有物体会被渲染到深度缓冲。(也就是之前提到的这一过程)
对于所有需要遮挡测试的物体向GPU提交一个遮挡查询请求。
在每一帧的最后,CPU从前一帧(或者更加前面的帧)读回物体的可见性结果。如果物体是可见的就将物体标为在下一帧需要渲染。对于不可见的物体,将其加入一个“分组”的查询中,该查询会以批次提交最高8个物体的包围盒组,测试这些物体在下一帧是否可见。
如果整个分组在下一帧变为可见,那么再将整个组重新分离为独立的遮挡查询并提交。
如果相机和物体是静止的(或者缓慢移动),这一优化会将必要的遮挡查询数量减少8倍。唯一一个我注意到的奇怪地方是被遮挡物体的批次查询组合方式似乎是随机的,而不是基于物体在空间上的距离。

这一步对应于上图中的IndividualQueries和GroupedQueries标记。GroupedQueries在这一帧是空的,因为UE4没有在前一帧中找到任何需要这一操作的物体。

在整个遮挡剔除pass的最后,ShadowFrustumQueries提交所有针对本地光源(local light,也就是点光源或者聚光灯)的包围盒的遮挡查询(无论光源是否投影都会提交,和这一步的名字所表达的意思不同),如果某个光源被完全遮挡住了那么就没必要去对该光源进行任何光照/投影计算。值得注意的是我们的示例场景中有4个点光源(每一帧每个光源都需要计算shadowmap),但是ShadowFrustumQueries这一步提交的查询数量为3。我猜测这是因为其中一个光源的包围盒和相机近裁面相交,因此UE4认为该光源必然可见。另一点值得一提,对于一个需要计算cubemap shadowmap的动态光源,UE4会提交一个球体来进行遮挡测试。

对于需要计算逐物体阴影的静态(static)动态光源(之后会有更详细的介绍),UE4会提交一个视锥体来进行遮挡检测:

最后对于PlanarReflectionQueries这一步,我估计是指用于计算平面反射(planar reflection)的遮挡剔除计算(方法是将相机变换到渲染平面之后/之下在重新绘制物体)。

Hi-Z缓冲的生成
接下来,UE4会创建一个Hi-Z缓冲(passes HZB SetupMipXX),存储格式为16位浮点数(R16_Float)。这一步将Z-prepass阶段得到的深度缓冲作为输入创建一个深度值的mipmap链(mipmap chain)。这一步还会将深度重新采样为分辨率大小为2的幂次数的纹理,这样用起来更方便。

之前提到,由于UE4使用reverse-Z,pixel shader在降采样时使用最小值操作符(译者注:也就是指每次降采样时选取邻域内深度值最小的像素输出到下一个mipmap)。

阴影的渲染
接下来一步是阴影计算render pass(ShadowDepths)。

在这个场景重我添加了一个固定光源的平行光(stationary directional light),两个可移动(movable)的点光源以及一个静态(static)的点光源。所有光源都会计算阴影。

对于固定光源(stationary light),渲染器会为静态物体烘焙阴影,并只为动态物体计算动态阴影。对于可移动的光源每一帧都需要为所有物体计算阴影(完全动态)。最终对于静态光源(static light)其阴影会被烘焙入光照贴图(lightmap),所以这些阴影在渲染中不会出现。

对于平行光我添加了分三个层级的级联阴影(cascaded shadowmaps),以观察UE4是怎么处理这个功能的。UE4创建了一个3x1的格式为R16_TYPELESS的纹理(每行3个tile,每层阴影一个),每一帧清除一次(意味着每一帧所有层都要更新,而不会有隔帧更新之类的优化)。随后,在Atlas0 render pass中所有物体会被渲染进对应的阴影tile中。

从上面的drawcall列表可看出只有Split0需要渲染一些物体,其他块是空的。阴影在渲染时无需pixel shader,这能使得阴影的渲染速度翻倍。值得注意的是无论平行光是静止的还是动态的,渲染器会将所有物体(包括静态物体)都渲染到阴影贴图中。

接下来是Atlas1 render pass,这一步将渲染所有固定点光源(stationary light)的阴影。在我的场景中只有那块岩石模型被标记为动态物体。对于固定光源(stationary light)和动态物体,UE4使用逐物体阴影贴图,保存在一个纹理图集(texture atlas)中,意味着对于每一个光源,每一个物体都会渲染一个shadowmap。

最后,对于动态光源,UE4使用传统的立方体阴影(cubemap shadowmap,在CubemapXX passes中),使用一个geometry shader来选择要渲染到cubemap的哪个面上(以减少draw call)。在这一步只渲染动态物体,所有静态/固定物体会被缓存起来。CopyCachedShadowMap这一步会把阴影缓存复制进来,然后在此之上渲染动态物体的阴影深度。下图是一个动态光源的立方体阴影缓存中一个面的内容(CopyCachedShadowMap这一步的输出)

这是渲染了动态物体(石头)后的结果:

静态物体的阴影缓存不会再每一帧重新生成,因为渲染器知道(我们场景中的)这一光源没有移动(尽管被标记为动态光源)。如果光源移动了,渲染器会在每一帧渲染动态物体前把所有静态物体重新绘制入阴影缓存中(这一步我在另一个测试中证实):

唯一一个静态光源(static light)完全没有出现在drawcall列表中,意味着这个光源不会影响动态物体,只会通过光照贴图去影响静态物体。

在本文最后提个建议,如果在你的场景中有固定光源(stationary light)请确保在编辑器中测试性能前烘焙光照(我不确定在standalone模式下运行时是否需要这样),如果不烘焙的话UE4会将它当做动态光源并渲染立方体阴影,而不是逐物体阴影。

在下一篇中我们会继续探索UE4的渲染流程,考察light grid生成,G-prepass和光照这些渲染步骤。
参考技术A ue渲染画面和序列不一样。是因为在渲染的图像画幅比编辑器预览的画幅小很多。导致K完的动画无法使用。解决办法为:在sequencer中创建相机。在这个相机中K帧。渲染的画面和编辑的画面就会同步 参考技术B ue渲染画面和序列不一样。是因为在渲染的图像画幅比编辑器预览的画幅小很多。导致K完的动画无法使用。解决办法为:在sequencer中创建相机。在这个相机中K帧。渲染的画面和编辑的画面就会同步.

以上是关于ue渲染就第一帧有问题的主要内容,如果未能解决你的问题,请参考以下文章

UE4渲染管线学习笔记

UE4帧渲染内容分享

UE4帧渲染内容分享

两个 FBO 之间的乒乓渲染在第一帧后失败。

Flutter之 flutter_after_layout组件的作用:监听页面渲染的第一帧

ue4和渲染谁赚钱