7

游戏中动态分辨率从原理到应用 - UWA问答 | 博客 | 游戏及VR应用性能优化记录分享 |...

 2 years ago
source link: https://blog.uwa4d.com/archives/USparkle_DynamicResolution.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

随着当前越来越多的手游向“3A”靠拢,手机上的各种性能优化也在努力地为“3A”保驾护航,恨不得要把芯片上每一个晶体管的性能都挖掘出来。但是,当一台“高分低能”的手机摆在你面前的时候,是不是总是有一种“欲哭无泪”的无力感——既要保持高帧率又要保证画面质量。成年人从来不做选择题,在两个都要的情况下,降分辨率往往是起效最快的办法。

说到调整设备的分辨率,Screen.SetResolution这个方法大家肯定是很熟悉了,但是这种调整是全局的,是硬件级别的调整,无法做到3D和UI渲染目标的分开调整。当然,随着SRP管线的推出,我们已经可以实现3D相机和UI相机分辨率的分开调整,并且UWA上已有相关文章的介绍了(见参考9)。

今天这篇文章要探讨的是,Unity和Unreal都提供的动态分辨率的方案,它可以动态缩放单个渲染目标,以减少GPU上的工作量。

说到3D和UI的分开渲染,聪明的小伙伴肯定想到了一种方案:3D渲染到一张RT上,最后把3D的RT Blit到最终的RT上。那么这种方案跟Unity提出的动态分辨率方案有何不同的地方吗?还是说只是新瓶装旧酒?

接下来,我就跟大家一起探索一下动态分辨率(以Unity为主)的原理以及它的应用场景。

传统的3D和UI分离方案

1.png

如上图所示,基本原理是渲染场景的时候调整视口大小(Viewport),将渲染约束到屏幕外Render Target的一部分,然后再把场景的Render Target上的内容Blit到最终的RT上。例如,渲染目标的大小可能为(1920,1080),但视口的原点可能为(0,0),大小为 (1280,720)。

这种实现方式可能会有如下几个问题:

  1. Blit的性能损耗,这个操作肯定不能是实时的,一般也就是在游戏初始化后或者在进入某个场景前设置一次,是一个低频操作,无法做到真正的“实时”调整。

  2. 可能受限于渲染管线

  • 如果是默认渲染管线的话,最后这个Blit的操作时机就要选好,因为游戏中一般会有后处理阶段,我们要利用好这个阶段顺便把Blit也做了。这个可以利用CommandBuffer向相机的不同渲染阶段插入视口修改和后处理操作。
  • 如果是SRP渲染管线的话(Unity 2018以后的版本),我们就能有自己处理Blit的时机了,当然这个操作也不能是个高频操作。

使用流程

参考Unity官方文档,我们先来看一下动态分辨率的使用流程。

首先我们要确认一点:动态分辨率启用的前提是GPU Bound了。所以要通过实时获取每帧GPU的运行时间来决定:

  • 是否是GPU压力过大导致游戏掉帧
  • 渲染目标的缩放系数

再根据缩放系数对渲染目标进行动态缩放。在这个过程中需要保证修改渲染目标分辨率的时候不重新分配GPU显存,否则就跟Screen.SetResolution一样了(会导致画面闪烁)。

  1. 在需要动态缩放的相机上勾选,如图所示:
2.png
  1. 在PlayerSettings中勾选上“Enable Frame Timing Stats”:
3.png
  • 通过两个接口FrameTimingManager.CaptureFrameTimings()和FrameTimingManager.GetLatestTimings获取CPUTime和GPUTime后自行判断缩放系数
  • 最后调用ScalableBufferManager.ResizeBuffers(m_widthScale, m_heightScale)设置缩放

平台支持

Unity官方文档上是这么写的:

4.png

5.png

可能跟理解水平有关系,看到以上的说明,我就犯迷糊了:OpenGLES不支持动态分辨率,内置渲染管线、URP等兼容,那么如果是URP下的OpenGLES平台呢?支持还是不支持?

不管如何,先把疑惑放一边,我们来探究一下动态分辨率的实现原理。

原理探究

我们顺着官方文档上的使用流程,摸入Unity源码内部,看看为什么对OpenGLES如此厚此薄彼。因为涉及到源码部分,这里就直接说结论了。

  • 缩放RT是跟平台相关的,OpenGLES无法创建缩放RT,原因我们后面再讲
  • 动态分辨率的原理为Vulkan的内存混叠(Memory Aliasing)功能

Memory Aliasing
Memory Aliasing可以翻译成内存混叠或内存别名,参考1是Vulkan针对此概念的说明。

现代图形 API(如DirectX 12或Vulkan)可以让用户定义内存位置,将分配的GPU资源放入手动创建的堆中。它允许我们创建纹理和缓冲区,它们的内存部分甚至可以完全重叠。这也是为什么OpenGLES不支持动态分辨率的原因,因为OpenGLES没有开放更底层的API让我们可以实现更高效的内存管理。

以游戏中典型的一帧为例:光栅化一些几何体,执行着色,然后运行一堆后处理。这里的每个阶段的输出都将写入纹理或缓冲区,稍后在一帧中被其他阶段使用。但是,某个阶段产生的资源可能只被少数其他阶段使用,比如在后处理中:Bloom产生的输出,只会被下一阶段的Tone mapping(色调映射)使用,并且在帧中的其他任何地方都不需要。我们可以看到,资源的有效生命周期可能很短,但很可能是预先分配的,并且在整个帧中都占用了它的内存。

解决内存频繁分配释放的方法就是对象池,Unity的RenderTexture.GetTemporary就是在内部维护了一个RenderTexture的对象池。但是这种方法只适用于后处理阶段,因为不同格式、大小的资源不能复用,后处理通常是全屏的Pass,读取、写入的Texture通常都有相同的属性,一些简单的后处理只需要两个RT反复交替使用就能实现(这个我会在稍后的URP章节中重点解读一下) 。

对象池本质上是一种更上层的Memory Aliasing,开发者不需要关注内存管理;但现代图形API(DX12和Vulkan)提供了内存管理的接口,可以实现底层的Memory Aliasing。Memory Aliasing指的是不同变量指向同一地址,即在同一片内存区域中同时存放多个资源,如果有很多大型资源在时间上不会重叠,就可以在相同的内存分配这些资源。相比对象池,Memory Aliasing可以进一步降低内存占用,因为在底层都是一堆字节,所以就不需要考虑资源的类型、格式、大小等。具体的示意如下图所示:

6.png

小结
从以上的分析我们大概了解到了Unity实现动态分辨率的原理:利用Vulkan提供的内存管理接口,实现底层对内存高效地复用。这样我们在游戏中就可以高效实时地调整分辨率,基本没有性能损耗。

URP实现

考虑到URP的前身LWRP还有项目组在用,下面先简单看一下LWRP。

LWRP
简单点说就是通过重新创建相机的渲染目标来实现的。Setup时会先进入函数RequiresIntermediateColorTexture判断是否要创建新的RT,里面就有个变量isScaledRender,如果需要缩放,则进入创建RT的Pass:

m_CreateLightweightRenderTexturesPass

public void Setup(ScriptableRenderer renderer, ref RenderingData renderingData)
{
    ...

    bool requiresRenderToTexture = ScriptableRenderer.RequiresIntermediateColorTexture(ref renderingData.cameraData, baseDescriptor);

    RenderTargetHandle colorHandle = RenderTargetHandle.CameraTarget;
    RenderTargetHandle depthHandle = RenderTargetHandle.CameraTarget;

    if (requiresRenderToTexture)
    {
          colorHandle = ColorAttachment;
          depthHandle = DepthAttachment;

          var sampleCount = (SampleCount)renderingData.cameraData.msaaSamples;
          m_CreateLightweightRenderTexturesPass.Setup(baseDescriptor, colorHandle, depthHandle, sampleCount);
          renderer.EnqueuePass(m_CreateLightweightRenderTexturesPass);
    }

    ...
}

public static bool RequiresIntermediateColorTexture(ref CameraData cameraData, RenderTextureDescriptor baseDescriptor)
{
     if (cameraData.isOffscreenRender)
          return false;

     bool isScaledRender = !Mathf.Approximately(cameraData.renderScale, 1.0f);
     bool isTargetTexture2DArray = baseDescriptor.dimension == TextureDimension.Tex2DArray;
     bool noAutoResolveMsaa = cameraData.msaaSamples > 1 && !SystemInfo.supportsMultisampleAutoResolve;
     return noAutoResolveMsaa || cameraData.isSceneViewCamera || isScaledRender || cameraData.isHdrEnabled ||
           cameraData.postProcessEnabled || cameraData.requiresOpaqueTexture || isTargetTexture2DArray || !cameraData.isDefaultViewport;
}

URP
从Unity 2019.3.0a这个版本开始,LWRP开始正式升级为URP。URP主要分为两个文件夹:一个是单独提取出来跟HDRP共用的基础核心库core,另一个就是URP自己用的universal。

7.png

翻看了URP各个版本的代码,直到Core RP库10.2版本(对应Unity版本为2020.2.0b)开始,Unity才开始重视(提供)Render Target(渲染目标)的管理功能。

从上一章节的“原理探究”中,我们知道渲染目标管理是任何渲染管线的重要组成部分;我们也知道RenderTexture只有在新渲染纹理使用完全相同的属性和分辨率时才能重用内存。

为了解决渲染纹理内存分配的这些问题,Unity的SRP(URP&HDRP)引入了RTHandle系统。该系统是RenderTexture之上的一个抽象层,可较好地管理渲染纹理,具体介绍可以看参考8,这里我就简单介绍一下。

8.png

如上截图中枚举所示,SRP实现了“硬件”和“软件”两种动态分辨率,“硬件动态分辨率“就是利用内存混叠硬实现的,而”软件动态分辨率“就是缩放RT适应当前视口的软实现。当硬件动态分辨率不支持当前平台时,RTHandle系统会自动切换为软件动态分辨率。不仅如此,最新的URP版本还基于RTHandle实现了双缓冲,感兴趣的可以去URP源码查看RenderTargetBufferSystem。

应用

一路下来,我们对“动态分辨率”也有了一个比较深刻的认识了,当说到“动态分辨率”时,我们说的就是真正的硬件层面实现的动态分辨率,即:能够充分利用现代图形API的Memory Aliasing,为把FPS维持在一定的水平,当发生GPU引起的掉帧时,能够在不重新分配GPU显存(利用图形API的Memory Aliasing)的情况下动态调整渲染目标分辨率。

但是,考虑到设备的兼容性,我们大部分游戏支持的平台都只能是OpenGLES而不是Vulkan,因此很遗憾,动态分辨率派不上用场了。退而求其次,针对不同的渲染管线,下面简单说明一下我们能够采用的方案:

  • 默认渲染管线——Unity 2017(含)以前的版本
  • 可以使用本文“传统的3D和UI分离方案”中介绍的方案,利用CommandBuffer在合适的时机对视口进行动态调整,但不能高频使用。
  • LWRP——Unity 2018~Unity 2019.3.0a
  • URP——Unity 2019.3.0a12+~Unity 2020.2.0b8+
    LWRP作为URP的前身,有好多功能还在完善中,已经可以比较好地实现3D和UI的分开渲染,相比默认渲染管线灵活性更好了。但还是没有提供比较好的RT管理,需要自己参考URP来定制一套高效的RT管理系统。
  • URP——Unity 2020.2.0b12+

如上所述,直到SRP的Core RP库10.2版本开始,Unity才提供了一套比较完善的RT管理系统,大家可以酌情参考使用。


[1] https://www.khronos.org/registry/vulkan/specs/1.0/html/chap12.html#resources-memory-aliasing

[2] 内存混叠的一种实现

[3] https://developer.nvidia.com/vulkan-memory-management

[4] https://docs.unrealengine.com/4.26/zh-CN/RenderingAndGraphics/DynamicResolution/

[5] https://www.intel.com/content/www/us/en/developer/articles/technical/dynamic-resolution-rendering-article.html

[6] https://docs.unity3d.com/Manual/DynamicResolution.html

[7] https://github.com/Unity-Technologies/DynamicResolutionSample

[8] https://docs.unity3d.com/Packages/[email protected]/manual/index.html

[9] 如何只降3D相机不降UI相机的分辨率


这是侑虎科技第1198篇文章,感谢作者吕强供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)

作者在UWA学堂上线的《五天实现PBR保姆级教程》课程限时优惠中~

再次感谢吕强的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK